new script: mmnode-ticker

Display cryptocurrency and other asset prices in convenient tabular format,
with optional display of your portfolio.  Output is highly configurable.

Usage information:

    $ mmnode-ticker --help
This commit is contained in:
The MMGen Project 2022-08-04 14:16:28 +00:00
commit e7fcc00b95
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
17 changed files with 1416 additions and 7 deletions

17
cmds/mmnode-ticker Executable file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
# https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
"""
mmnode-ticker: Display price information for cryptocurrency and other assets
"""
from mmgen.main import launch
launch('ticker',package='mmgen_node_tools')

102
mmgen_node_tools/Misc.py Executable file
View file

@ -0,0 +1,102 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
# https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
"""
mmgen_node_tools.Misc: miscellaneous data and functions for the MMGen Node Tools suite
"""
curl_exit_codes = {
1: 'Unsupported protocol. This build of curl has no support for this protocol',
2: 'Failed to initialize',
3: 'URL malformed. The syntax was not correct',
4: 'A feature or option that was needed to perform the desired request was not enabled or was explicitly disabled at build-time. To make curl able to do this, you probably need another build of libcurl!',
5: 'Couldn’t resolve proxy. The given proxy host could not be resolved',
6: 'Couldn’t resolve host. The given remote host was not resolved',
7: 'Failed to connect to host',
8: 'Weird server reply. The server sent data curl couldn’t parse',
9: 'FTP access denied. The server denied login or denied access to the particular resource or directory you wanted to reach. Most often you tried to change to a directory that doesn’t exist on the server',
10: 'FTP accept failed. While waiting for the server to connect back when an active FTP session is used, an error code was sent over the control connection or similar',
11: 'FTP weird PASS reply. Curl couldn’t parse the reply sent to the PASS request',
12: 'During an active FTP session while waiting for the server to connect back to curl, the timeout expired',
13: 'FTP weird PASV reply, Curl couldn’t parse the reply sent to the PASV request',
14: 'FTP weird 227 format. Curl couldn’t parse the 227-line the server sent',
15: 'FTP can’t get host. Couldn’t resolve the host IP we got in the 227-line',
16: 'HTTP/2 error. A problem was detected in the HTTP2 framing layer. This is somewhat generic and can be one out of several problems, see the error message for details',
17: 'FTP couldn’t set binary. Couldn’t change transfer method to binary',
18: 'Partial file. Only a part of the file was transferred',
19: 'FTP couldn’t download/access the given file, the RETR (or similar) command failed',
21: 'FTP quote error. A quote command returned error from the server',
22: 'HTTP page not retrieved. The requested url was not found or returned another error with the HTTP error code being 400 or above. This return code only appears if -f, --fail is used',
23: 'Write error. Curl couldn’t write data to a local filesystem or similar',
25: 'FTP couldn’t STOR file. The server denied the STOR operation, used for FTP uploading',
26: 'Read error. Various reading problems',
27: 'Out of memory. A memory allocation request failed',
28: 'Operation timeout. The specified time-out period was reached according to the conditions',
30: 'FTP PORT failed. The PORT command failed. Not all FTP servers support the PORT command, try doing a transfer using PASV instead!',
31: 'FTP couldn’t use REST. The REST command failed. This command is used for resumed FTP transfers',
33: 'HTTP range error. The range "command" didn’t work',
34: 'HTTP post error. Internal post-request generation error',
35: 'SSL connect error. The SSL handshaking failed',
36: 'Bad download resume. Couldn’t continue an earlier aborted download',
37: 'FILE couldn’t read file. Failed to open the file. Permissions?',
38: 'LDAP cannot bind. LDAP bind operation failed',
39: 'LDAP search failed',
41: 'Function not found. A required LDAP function was not found',
42: 'Aborted by callback. An application told curl to abort the operation',
43: 'Internal error. A function was called with a bad parameter',
45: 'Interface error. A specified outgoing interface could not be used',
47: 'Too many redirects. When following redirects, curl hit the maximum amount',
48: 'Unknown option specified to libcurl. This indicates that you passed a weird option to curl that was passed on to libcurl and rejected. Read up in the manual!',
49: 'Malformed telnet option',
51: 'The peer’s SSL certificate or SSH MD5 fingerprint was not OK',
52: 'The server didn’t reply anything, which here is considered an error',
53: 'SSL crypto engine not found',
54: 'Cannot set SSL crypto engine as default',
55: 'Failed sending network data',
56: 'Failure in receiving network data',
58: 'Problem with the local certificate',
59: 'Couldn’t use specified SSL cipher',
60: 'Peer certificate cannot be authenticated with known CA certificates',
61: 'Unrecognized transfer encoding',
62: 'Invalid LDAP URL',
63: 'Maximum file size exceeded',
64: 'Requested FTP SSL level failed',
65: 'Sending the data requires a rewind that failed',
66: 'Failed to initialise SSL Engine',
67: 'The user name, password, or similar was not accepted and curl failed to log in',
68: 'File not found on TFTP server',
69: 'Permission problem on TFTP server',
70: 'Out of disk space on TFTP server',
71: 'Illegal TFTP operation',
72: 'Unknown TFTP transfer ID',
73: 'File already exists (TFTP)',
74: 'No such user (TFTP)',
75: 'Character conversion failed',
76: 'Character conversion functions required',
77: 'Problem with reading the SSL CA cert (path? access rights?)',
78: 'The resource referenced in the URL does not exist',
79: 'An unspecified error occurred during the SSH session',
80: 'Failed to shut down the SSL connection',
82: 'Could not load CRL file, missing or wrong format (added in 7.19.0)',
83: 'Issuer check failed (added in 7.19.0)',
84: 'The FTP PRET command failed',
85: 'RTSP: mismatch of CSeq numbers',
86: 'RTSP: mismatch of Session Identifiers',
87: 'unable to parse FTP file list',
88: 'FTP chunk callback reported error',
89: 'No connection available, the session will be queued',
90: 'SSL public key does not matched pinned public key',
91: 'Invalid SSL certificate status',
92: 'Stream error in HTTP/2 framing layer',
93: 'An API function was called from inside a callback',
94: 'An authentication function returned an error',
95: 'A problem was detected in the HTTP/3 layer. This is somewhat generic and can be one out of several problems, see the error message for details',
96: 'QUIC connection error. This error may be caused by an SSL library error. QUIC is the protocol used for HTTP/3 transfers',
}

777
mmgen_node_tools/Ticker.py Executable file
View file

@ -0,0 +1,777 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
# https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
"""
mmgen_node_tools.Ticker: Display price information for cryptocurrency and other assets
"""
api_host = 'api.coinpaprika.com'
api_url = f'https://{api_host}/v1/ticker'
ratelimit = 240
btc_ratelimit = 10
# We use deprecated coinpaprika ‘ticker’ API for now because it returns ~45% less data.
# Old ‘ticker’ API (/v1/ticker): data['BTC']['price_usd']
# New ‘tickers’ API (/v1/tickers): data['BTC']['quotes']['USD']['price']
# Possible alternatives:
# - https://min-api.cryptocompare.com/data/pricemultifull?fsyms=BTC,LTC&tsyms=USD,EUR
import sys,os,time,json,yaml
from subprocess import run,PIPE,CalledProcessError
from decimal import Decimal
from collections import namedtuple
from mmgen.opts import opt
from mmgen.globalvars import g
from mmgen.color import *
from mmgen.util import die,fmt_list,msg,msg_r,Msg,do_pager,suf,fmt
homedir = os.getenv('HOME')
cachedir = os.path.join(homedir,'.cache','mmgen-node-tools')
cfg_fn = 'ticker-cfg.yaml'
portfolio_fn = 'ticker-portfolio.yaml'
def assets_list_gen(cfg_in):
for k,v in cfg_in.cfg['assets'].items():
yield('')
yield(k.upper())
for e in v:
yield(' {:4s} {}'.format(*e.split('-',1)))
def gen_data(data):
"""
Filter the raw data and return it as a dict keyed by the IDs of the assets
we want to display.
Add dummy entry for USD and entry for user-specified asset, if any.
Since symbols in source data are not guaranteed to be unique (e.g. XAG), we
must search the data twice: first for unique IDs, then for symbols while
checking for duplicates.
"""
def dup_sym_errmsg(dup_sym):
return (
f'The symbol {dup_sym!r} is shared by the following assets:\n' +
'\n ' + '\n '.join(d['id'] for d in data if d['symbol'] == dup_sym) +
'\n\nPlease specify the asset by one of the full IDs listed above\n' +
f'instead of {dup_sym!r}'
)
def check_assets_found(wants,found,keys=['symbol','id']):
error = False
for k in keys:
missing = wants[k] - found[k]
if missing:
msg(
('The following IDs were not found in source data:\n{}' if k == 'id' else
'The following symbols could not be resolved:\n{}').format(
fmt_list(missing,fmt='col',indent=' ')
))
error = True
if error:
die(1,'Missing data, exiting')
rows_want = {
'id': {r.id for r in cfg.rows if getattr(r,'id',None)} - {'usd-us-dollar'},
'symbol': {r.symbol for r in cfg.rows if isinstance(r,tuple) and r.id is None} - {'USD'},
}
usr_assets = cfg.usr_rows + cfg.usr_columns + tuple(c for c in (cfg.query or ()) if c)
usr_wants = {
'id': (
{a.id for a in usr_assets if a.id} -
{a.id for a in usr_assets if a.amount and a.id} - {'usd-us-dollar'} )
,
'symbol': (
{a.symbol for a in usr_assets if not a.id} -
{a.symbol for a in usr_assets if a.amount} - {'USD'} ),
}
found = { 'id': set(), 'symbol': set() }
for k in ['id','symbol']:
wants = rows_want[k] | usr_wants[k]
if wants:
for d in data:
if d[k] in wants:
if d[k] in found[k]:
die(1,dup_sym_errmsg(d[k]))
yield (d['id'],d)
found[k].add(d[k])
if k == 'id' and len(found[k]) == len(wants):
break
for d in data:
if d['id'] == 'btc-bitcoin':
btcusd = Decimal(d['price_usd'])
break
for asset in (cfg.usr_rows + cfg.usr_columns):
if asset.amount:
"""
User-supplied rate overrides rate from source data.
"""
_id = asset.id or f'{asset.symbol}-user-asset-{asset.symbol}'.lower()
yield ( _id, {
'symbol': asset.symbol,
'id': _id,
'price_usd': str(Decimal(1/asset.amount)),
'price_btc': str(Decimal(1/asset.amount/btcusd)),
'last_updated': int(now),
})
yield ('usd-us-dollar', {
'symbol': 'USD',
'id': 'usd-us-dollar',
'price_usd': '1.0',
'price_btc': str(Decimal(1/btcusd)),
'last_updated': int(now),
})
check_assets_found(usr_wants,found)
def get_src_data(curl_cmd):
tor_captcha_msg = f"""
If youre using Tor, the API request may have failed due to Captcha protection.
A workaround for this issue is to retrieve the JSON data with a browser from
the following URL:
{api_url}
and save it to:
{cfg.cachedir}/ticker.json
Then invoke the program with --cached-data and without --btc
"""
def rate_limit_errmsg(timeout,elapsed):
return (
f'Rate limit exceeded! Retry in {timeout-elapsed} seconds' +
('' if cfg.btc_only else ', or use --cached-data or --btc')
)
if not os.path.exists(cachedir):
os.makedirs(cachedir)
if cfg.btc_only:
fn = os.path.join(cfg.cachedir,'ticker-btc.json')
timeout = 5 if g.test_suite else btc_ratelimit
else:
fn = os.path.join(cfg.cachedir,'ticker.json')
timeout = 5 if g.test_suite else ratelimit
fn_rel = os.path.relpath(fn,start=homedir)
if not os.path.exists(fn):
open(fn,'w').write('{}')
if opt.cached_data:
json_text = open(fn).read()
else:
elapsed = int(time.time() - os.stat(fn).st_mtime)
if elapsed >= timeout:
msg_r(f'Fetching data from {api_host}...')
try:
cp = run(curl_cmd,check=True,stdout=PIPE)
except CalledProcessError as e:
msg('')
from .Misc import curl_exit_codes
msg(red(curl_exit_codes[e.returncode]))
msg(red('Command line:\n {}'.format( ' '.join((repr(i) if ' ' in i else i) for i in e.cmd) )))
from mmgen.exception import MMGenCalledProcessError
raise MMGenCalledProcessError(f'Subprocess returned non-zero exit status {e.returncode}')
json_text = cp.stdout.decode()
msg('done')
else:
die(1,rate_limit_errmsg(timeout,elapsed))
try:
data = json.loads(json_text)
except:
msg(json_text[:1024] + '...')
msg(orange(fmt(tor_captcha_msg,strip_char='\t')))
die(2,'Retrieved data is not valid JSON, exiting')
if not data:
if opt.cached_data:
die(1,'No cached data! Run command without --cached-data option to retrieve data from remote host')
else:
die(2,'Remote host returned no data!')
elif 'error' in data:
die(1,data['error'])
if opt.cached_data:
msg(f'Using cached data from ~/{fn_rel}')
else:
open(fn,'w').write(json_text)
msg(f'JSON data cached to ~/{fn_rel}')
return data
def main(cfg_parm,cfg_in_parm):
def update_sample_file(usr_cfg_file):
src_data = files('mmgen_node_tools').joinpath('data',os.path.basename(usr_cfg_file)).read_text()
sample_file = usr_cfg_file + '.sample'
sample_data = open(sample_file).read() if os.path.exists(sample_file) else None
if src_data != sample_data:
os.makedirs(os.path.dirname(sample_file),exist_ok=True)
msg('{} {}'.format(
('Updating','Creating')[sample_data is None],
sample_file ))
open(sample_file,'w').write(src_data)
def get_curl_cmd():
return ([
'curl',
'--tr-encoding',
'--compressed', # adds 'Accept-Encoding: gzip'
'--silent',
'--header', 'Accept: application/json',
] +
(['--proxy', cfg.proxy] if cfg.proxy else []) +
[api_url + ('/btc-bitcoin' if cfg.btc_only else '')]
)
global cfg,cfg_in
cfg = cfg_parm
cfg_in = cfg_in_parm
try:
from importlib.resources import files # Python 3.9
except ImportError:
from importlib_resources import files
update_sample_file(cfg_in.cfg_file)
update_sample_file(cfg_in.portfolio_file)
if opt.portfolio and not cfg_in.portfolio:
die(1,'No portfolio configured!\nTo configure a portfolio, edit the file ~/{}'.format(
os.path.relpath(cfg_in.portfolio_file,start=homedir)))
curl_cmd = get_curl_cmd()
if opt.print_curl:
Msg(curl_cmd + '\n' + ' '.join(curl_cmd))
return
parsed_json = [get_src_data(curl_cmd)] if cfg.btc_only else get_src_data(curl_cmd)
if opt.list_ids:
do_pager('\n'.join(e['id'] for e in parsed_json))
return
global now
now = 1659465400 if g.test_suite else time.time() # 1659524400 1659445900
(do_pager if opt.pager else Msg)(
'\n'.join(getattr(Ticker,cfg.clsname)(dict(gen_data(parsed_json))).gen_output())
)
def make_cfg(cmd_args,cfg_in):
def get_rows_from_cfg(add_data=None):
def create_row(e):
return asset_tuple(e.split('-')[0].upper(),e)
def gen():
for n,(k,v) in enumerate(cfg_in.cfg['assets'].items()):
yield(k)
if add_data and k in add_data:
v += tuple(add_data[k])
for e in v:
yield(create_row(e))
return tuple(gen())
def parse_asset_tuple(s):
sym,id = (s.split('-')[0],s) if '-' in s else (s,None)
return asset_tuple( sym.upper(), id.lower() if id else None )
def parse_asset_triplet(s):
ss = s.split(':')
sym,amt = ( ss[0], Decimal(ss[1]) ) if len(ss) == 2 else ( s, None )
return asset_triplet( *parse_asset_tuple(sym), amt )
def parse_usr_asset_arg(s):
return tuple(parse_asset_triplet(ss) for ss in s.split(',')) if s else ()
def parse_query_arg(s):
ss = s.split(':')
if len(ss) == 2:
return query_tuple(
asset = parse_asset_triplet(s),
to_asset = None )
elif len(ss) in (3,4):
return query_tuple(
asset = parse_asset_triplet(':'.join(ss[:2])),
to_asset = parse_asset_triplet(':'.join(ss[2:])),
)
else:
die(1,f'{s}: malformed argument')
def gen_uniq(obj_list,key,preload=None):
found = set([getattr(obj,key) for obj in preload if hasattr(obj,key)] if preload else ())
for obj in obj_list:
id = getattr(obj,key)
if id not in found:
yield obj
found.add(id)
def get_usr_assets():
return (
'user_added',
usr_rows +
(tuple(asset for asset in query if asset) if query else ()) +
usr_columns )
def get_portfolio_assets(ret=()):
if cfg_in.portfolio and opt.portfolio:
ret = tuple( asset_tuple(e.split('-')[0].upper(),e) for e in cfg_in.portfolio )
return ( 'portfolio', tuple(e for e in ret if (not opt.btc) or e.symbol == 'BTC') )
def get_portfolio():
return {k:Decimal(v) for k,v in cfg_in.portfolio.items() if (not opt.btc) or k == 'btc-bitcoin'}
def parse_add_precision(s):
if not s:
return 0
if not (s.isdigit() and s.isascii()):
die(1,f'{s}: invalid parameter for --add-precision (not an integer)')
if int(s) > 30:
die(1,f'{s}: invalid parameter for --add-precision (value >30)')
return int(s)
def create_rows():
rows = (
('trade_pair',) + query if (query and query.to_asset) else
('bitcoin',parse_asset_tuple('btc-bitcoin')) if opt.btc else
get_rows_from_cfg( add_data={'fiat':['usd-us-dollar']} if opt.add_columns else None )
)
for hdr,data in (
(get_usr_assets(),) if query else
(get_usr_assets(), get_portfolio_assets())
):
if data:
uniq_data = tuple(gen_uniq(data,'symbol',preload=rows))
if uniq_data:
rows += (hdr,) + uniq_data
return rows
cfg_tuple = namedtuple('global_cfg',[
'rows',
'usr_rows',
'usr_columns',
'query',
'adjust',
'clsname',
'btc_only',
'add_prec',
'cachedir',
'proxy',
'portfolio' ])
query_tuple = namedtuple('query',['asset','to_asset'])
asset_triplet = namedtuple('asset_triplet',['symbol','id','amount'])
asset_tuple = namedtuple('asset_tuple',['symbol','id'])
usr_rows = parse_usr_asset_arg(opt.add_rows)
usr_columns = parse_usr_asset_arg(opt.add_columns)
query = parse_query_arg(cmd_args[0]) if cmd_args else None
return cfg_tuple(
rows = create_rows(),
usr_rows = usr_rows,
usr_columns = usr_columns,
query = query,
adjust = ( lambda x: (100 + x) / 100 if x else 1 )( Decimal(opt.adjust or 0) ),
clsname = 'trading' if query else 'overview',
btc_only = opt.btc,
add_prec = parse_add_precision(opt.add_precision),
cachedir = opt.cachedir or cfg_in.cfg.get('cachedir') or cachedir,
proxy = None if opt.proxy == '' else (opt.proxy or cfg_in.cfg.get('proxy')),
portfolio = get_portfolio() if cfg_in.portfolio and opt.portfolio and not query else None
)
def get_cfg_in():
ret = namedtuple('cfg_in_data',['cfg','portfolio','cfg_file','portfolio_file'])
cfg_file,portfolio_file = (
[os.path.join(g.data_dir_root,'node_tools',fn) for fn in (cfg_fn,portfolio_fn)]
)
cfg_data,portfolio_data = (
[yaml.safe_load(open(fn).read()) if os.path.exists(fn) else None for fn in (cfg_file,portfolio_file)]
)
return ret(
cfg = cfg_data or {
'assets': {
'coin': [ 'btc-bitcoin', 'eth-ethereum', 'xmr-monero' ],
'commodity': [ 'xau-gold-spot-token', 'xag-silver-spot-token', 'xbr-brent-crude-oil-spot' ],
'fiat': [ 'gbp-pound-sterling-token', 'eur-euro-token' ],
'index': [ 'dj30-dow-jones-30-token', 'spx-sp-500', 'ndx-nasdaq-100-token' ],
},
'proxy': 'http://vpn-gw:8118'
},
portfolio = portfolio_data,
cfg_file = cfg_file,
portfolio_file = portfolio_file,
)
class Ticker:
class base:
offer = None
to_asset = None
def __init__(self,data):
self.comma = ',' if opt.thousands_comma else ''
self.col1_wid = max(len('TOTAL'),(
max(len(self.create_label(d['id'])) for d in data.values()) if opt.name_labels else
max(len(d['symbol']) for d in data.values())
)) + 1
self.rows = [row._replace(id=self.get_id(row)) if isinstance(row,tuple) else row for row in cfg.rows]
self.col_usd_prices = {k:Decimal(self.data[k]['price_usd']) for k in self.col_ids}
self.prices = {row.id:self.get_row_prices(row.id)
for row in self.rows if isinstance(row,tuple) and row.id in data}
self.prices['usd-us-dollar'] = self.get_row_prices('usd-us-dollar')
def format_last_update_col(self,cross_assets=()):
if opt.elapsed:
from .Util import format_elapsed_hr
fmt_func = format_elapsed_hr
else:
fmt_func = lambda t,now: time.strftime('%F %X',time.gmtime(t)) # ticker API
# t.replace('T',' ').replace('Z','') # tickers API
d = self.data
max_w = 0
min_t = min( (int(d[a.id]['last_updated']) for a in cross_assets), default=None )
for row in self.rows:
if isinstance(row,tuple):
try:
t = int(d[row.id]['last_updated'])
except KeyError:
pass
else:
t_fmt = d[row.id]['last_updated_fmt'] = fmt_func( (min(t,min_t) if min_t else t), now )
max_w = max(len(t_fmt),max_w)
self.upd_w = max_w
def init_prec(self):
exp = [(a.id,Decimal.adjusted(self.prices[a.id]['usd-us-dollar'])) for a in self.usr_col_assets]
self.uprec = { k: max(0,v+4) + cfg.add_prec for k,v in exp }
self.uwid = { k: 12 + max(0, abs(v)-6) + cfg.add_prec for k,v in exp }
def get_id(self,asset):
if asset.id:
return asset.id
else:
for d in self.data.values():
if d['symbol'] == asset.symbol:
return d['id']
def create_label(self,id):
return ' '.join(id.split('-')[1:]).upper()
def gen_output(self):
yield 'Current time: {} UTC'.format(time.strftime('%F %X',time.gmtime(now)))
for asset in self.usr_col_assets:
if asset.symbol != 'USD':
usdprice = Decimal(self.data[asset.id]['price_usd'])
yield '{} ({}) = {:{}.{}f} USD'.format(
asset.symbol,
self.create_label(asset.id),
usdprice,
self.comma,
max(2,int(-usdprice.adjusted())+4) )
if hasattr(self,'subhdr'):
yield self.subhdr
if self.show_adj:
yield (
('Offered price differs from spot' if self.offer else 'Adjusting prices')
+ ' by '
+ yellow('{:+.2f}%'.format( (self.adjust-1) * 100 ))
)
yield ''
if cfg.portfolio:
yield blue('PRICES')
if self.table_hdr:
yield self.table_hdr
for row in self.rows:
if isinstance(row,str):
yield ('-' * self.hl_wid)
else:
try:
yield self.fmt_row(self.data[row.id])
except KeyError:
yield gray(f'(no data for {row.id})')
yield '-' * self.hl_wid
if cfg.portfolio:
self.fs_num = self.fs_num2
self.fs_str = self.fs_str2
yield ''
yield blue('PORTFOLIO')
yield self.table_hdr
yield '-' * self.hl_wid
for sym,amt in cfg.portfolio.items():
try:
yield self.fmt_row(self.data[sym],amt=amt)
except KeyError:
yield gray(f'(no data for {sym})')
yield '-' * self.hl_wid
if not cfg.btc_only:
yield self.fs_num.format(
lbl = 'TOTAL', pc1='', pc2='', upd='', amt='',
**{ k.replace('-','_'): v for k,v in self.prices['total'].items() }
)
class overview(base):
def __init__(self,data):
self.data = data
self.adjust = cfg.adjust
self.show_adj = self.adjust != 1
self.usr_col_assets = [asset._replace(id=self.get_id(asset)) for asset in cfg.usr_columns]
self.col_ids = ('usd-us-dollar',) + tuple(a.id for a in self.usr_col_assets) + ('btc-bitcoin',)
super().__init__(data)
self.format_last_update_col()
if cfg.portfolio:
self.prices['total'] = { col_id: sum(self.prices[row.id][col_id] * cfg.portfolio[row.id]
for row in self.rows if isinstance(row,tuple) and row.id in cfg.portfolio and row.id in data)
for col_id in self.col_ids }
self.init_prec()
self.init_fs()
def get_row_prices(self,id):
if id in self.data:
d = self.data[id]
return { k: (
Decimal(d['price_btc']) if k == 'btc-bitcoin' else
Decimal(d['price_usd']) / self.col_usd_prices[k]
) * self.adjust for k in self.col_ids }
def fmt_row(self,d,amt=None,amt_fmt=None):
def fmt_pct(d):
if d in ('',None):
return gray(' --')
n = Decimal(d)
return (red,green)[n>=0](f'{n:+7.2f}')
p = self.prices[d['id']]
if amt is not None:
amt_fmt = f'{amt:{19+cfg.add_prec}{self.comma}.{8+cfg.add_prec}f}'
if '.' in amt_fmt:
amt_fmt = amt_fmt.rstrip('0').rstrip('.')
return self.fs_num.format(
lbl = (self.create_label(d['id']) if opt.name_labels else d['symbol']),
pc1 = fmt_pct(d.get('percent_change_7d')),
pc2 = fmt_pct(d.get('percent_change_24h')),
upd = d.get('last_updated_fmt'),
amt = amt_fmt,
**{ k.replace('-','_'): v * (1 if amt is None else amt) for k,v in p.items() }
)
def init_fs(self):
col_prec = {'usd-us-dollar':2+cfg.add_prec,'btc-bitcoin':8+cfg.add_prec } # | self.uprec # Python 3.9
col_prec.update(self.uprec)
col_wid = {'usd-us-dollar':8+cfg.add_prec,'btc-bitcoin':12+cfg.add_prec } # """
col_wid.update(self.uwid)
max_row = max(
( (k,v['btc-bitcoin']) for k,v in self.prices.items() ),
key = lambda a: a[1]
)
widths = { k: len('{:{}.{}f}'.format( self.prices[max_row[0]][k], self.comma, col_prec[k] ))
for k in self.col_ids }
fd = namedtuple('format_str_data',['fs_str','fs_num','wid'])
col_fs_data = {
'label': fd(f'{{lbl:{self.col1_wid}}}',f'{{lbl:{self.col1_wid}}}',self.col1_wid),
'pct7d': fd(' {pc1:7}', ' {pc1:7}', 8),
'pct24h': fd(' {pc2:7}', ' {pc2:7}', 8),
'update_time': fd(' {upd}', ' {upd}', max((19 if cfg.portfolio else 0),self.upd_w) + 2),
'amt': fd(' {amt}', ' {amt}', 21),
}
# } | { k: fd( # Python 3.9
col_fs_data.update({ k: fd(
' {{{}:>{}}}'.format( k.replace('-','_'), widths[k] ),
' {{{}:{}{}.{}f}}'.format( k.replace('-','_'), widths[k], self.comma, col_prec[k] ),
widths[k]+2
) for k in self.col_ids
})
cols = (
['label','usd-us-dollar'] +
[asset.id for asset in self.usr_col_assets] +
[a for a,b in (
( 'btc-bitcoin', not cfg.btc_only ),
( 'pct7d', opt.percent_change ),
( 'pct24h', opt.percent_change ),
( 'update_time', opt.update_time ),
) if b]
)
cols2 = list(cols)
if opt.update_time:
cols2.pop()
cols2.append('amt')
self.fs_str = ''.join(col_fs_data[c].fs_str for c in cols)
self.fs_num = ''.join(col_fs_data[c].fs_num for c in cols)
self.hl_wid = sum(col_fs_data[c].wid for c in cols)
self.fs_str2 = ''.join(col_fs_data[c].fs_str for c in cols2)
self.fs_num2 = ''.join(col_fs_data[c].fs_num for c in cols2)
self.hl_wid2 = sum(col_fs_data[c].wid for c in cols2)
@property
def table_hdr(self):
return self.fs_str.format(
lbl = '',
pc1 = ' CHG_7d',
pc2 = 'CHG_24h',
upd = 'UPDATED',
amt = ' AMOUNT',
usd_us_dollar = 'USD',
btc_bitcoin = ' BTC',
**{ a.id.replace('-','_'): a.symbol for a in self.usr_col_assets }
)
class trading(base):
def __init__(self,data):
self.data = data
self.asset = cfg.query.asset._replace(id=self.get_id(cfg.query.asset))
self.to_asset = (
cfg.query.to_asset._replace(id=self.get_id(cfg.query.to_asset))
if cfg.query.to_asset else None )
self.col_ids = [self.asset.id]
self.adjust = cfg.adjust
if self.to_asset:
self.offer = self.to_asset.amount
if self.offer:
real_price = (
self.asset.amount
* Decimal(data[self.asset.id]['price_usd'])
/ Decimal(data[self.to_asset.id]['price_usd'])
)
if self.adjust != 1:
die(1,'the --adjust option may not be combined with TO_AMOUNT in the trade specifier')
self.adjust = self.offer / real_price
self.hl_ids = [self.asset.id,self.to_asset.id]
else:
self.hl_ids = [self.asset.id]
self.show_adj = self.adjust != 1 or self.offer
super().__init__(data)
self.usr_col_assets = [self.asset] + ([self.to_asset] if self.to_asset else [])
for a in self.usr_col_assets:
self.prices[a.id]['usd-us-dollar'] = Decimal(data[a.id]['price_usd'])
self.format_last_update_col(cross_assets=self.usr_col_assets)
self.init_prec()
self.init_fs()
def get_row_prices(self,id):
if id in self.data:
d = self.data[id]
return { k: self.col_usd_prices[self.asset.id] / Decimal(d['price_usd']) for k in self.col_ids }
def init_fs(self):
self.max_wid = max(
len('{:{}{}.{}f}'.format(
v[self.asset.id] * self.asset.amount,
16 + cfg.add_prec,
self.comma,
8 + cfg.add_prec
))
for v in self.prices.values()
)
self.fs_str = '{lbl:%s} {p_spot}' % self.col1_wid
self.hl_wid = self.col1_wid + self.max_wid + 1
if self.show_adj:
self.fs_str += ' {p_adj}'
self.hl_wid += self.max_wid + 1
if opt.update_time:
self.fs_str += ' {upd}'
self.hl_wid += self.upd_w + 2
def fmt_row(self,d):
id = d['id']
p = self.prices[id][self.asset.id] * self.asset.amount
p_spot = '{:{}{}.{}f}'.format( p, self.max_wid, self.comma, 8+cfg.add_prec )
p_adj = (
'{:{}{}.{}f}'.format( p*self.adjust, self.max_wid, self.comma, 8+cfg.add_prec )
if self.show_adj else '' )
return self.fs_str.format(
lbl = (self.create_label(id) if opt.name_labels else d['symbol']),
p_spot = green(p_spot) if id in self.hl_ids else p_spot,
p_adj = yellow(p_adj) if id in self.hl_ids else p_adj,
upd = d.get('last_updated_fmt'),
)
@property
def table_hdr(self):
return self.fs_str.format(
lbl = '',
p_spot = '{t:>{w}}'.format(
t = 'SPOT PRICE',
w = self.max_wid ),
p_adj = '{t:>{w}}'.format(
t = ('OFFERED' if self.offer else 'ADJUSTED') + ' PRICE',
w = self.max_wid ),
upd = 'UPDATED'
)
@property
def subhdr(self):
return (
'{a}: {b:{c}} {d}'.format(
a = 'Offer' if self.offer else 'Amount',
b = self.asset.amount,
c = self.comma,
d = self.asset.symbol
) + (
(
' =>' +
(' {:{}}'.format(self.offer,self.comma) if self.offer else '') +
' {} ({})'.format(
self.to_asset.symbol,
self.create_label(self.to_asset.id) )
) if self.to_asset else '' )
)

View file

@ -19,8 +19,8 @@
mmgen_node_tools.Util: utility functions for MMGen node tools
"""
import time,subprocess
from mmgen.util import msg
import time
from mmgen.util import suf
def get_hms(t=None,utc=False,no_secs=False):
secs = t or time.time()
@ -33,11 +33,26 @@ def get_day_hms(t=None,utc=False):
ret = (time.localtime,time.gmtime)[utc](secs)
return '{:04}-{:02}-{:02} {:02}:{:02}:{:02}'.format(*ret[0:6])
def format_elapsed_hr(t,now=None,cached={}):
now = now or time.time()
e = int(now - t)
if not e in cached:
h = abs(e) // 3600
m = abs(e) // 60 % 60
cached[e] = '{a}{b} minute{c} {d}'.format(
a = '{} hour{}, '.format(h,suf(h)) if h else '',
b = m,
c = suf(m),
d = 'ago' if e > 0 else 'in the future' ) if (h or m) else 'just now'
return cached[e]
def do_system(cmd,testing=False,shell=False):
if testing:
from mmgen.util import msg
msg("Would execute: '%s'" % cmd)
return True
else:
import subprocess
return subprocess.call((cmd if shell else cmd.split()),shell,stderr=subprocess.PIPE)
def get_url(url,gzip_ok=False,proxy=None,timeout=60,verbose=False,debug=False):

View file

@ -0,0 +1,30 @@
# Indentation must be consistent! Do not mix leading tabs and spaces.
# See the curl manpage for supported --proxy parameters
# For a direct connection, leave the right-hand side blank
proxy: http://vpn-gw:8118
# Override the default cache directory (~/.cache/mmgen-node-tools)
cachedir:
# Asset labels are arbitrary strings. Use as many or few as you wish.
# Invoke ‘mmnode-ticker --list-ids’ for a full list of supported asset IDs.
assets:
coin1:
- btc-bitcoin
- eth-ethereum
- xmr-monero
coin2:
- ada-cardano
- bnb-binance-coin
commodity:
- xau-gold-spot-token
- xag-silver-spot-token
- xbr-brent-crude-oil-spot
fiat:
- gbp-pound-sterling-token
- eur-euro-token
index:
- dj30-dow-jones-30-token
- spx-sp-500
- ndx-nasdaq-100-token

View file

@ -0,0 +1,6 @@
# List each asset and amount in your portfolio here.
# Amounts should be strings rather than floating-point values.
# Invoke `mmnode-ticker --list-ids` for a full list of supported asset IDs.
btc-bitcoin: '1.23456789'
eth-ethereum: '2.3456789012'
xmr-monero: '4.5678901234'

View file

@ -1 +1 @@
3.1.dev3
3.1.dev4

193
mmgen_node_tools/main_ticker.py Executable file
View file

@ -0,0 +1,193 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
# https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
"""
mmnode-ticker: Display price information for cryptocurrency and other assets
"""
import sys,os
from mmgen.common import *
from .Ticker import *
opts_data = {
'sets': [
('wide', True, 'percent_change', True),
('wide', True, 'name_labels', True),
('wide', True, 'thousands_comma', True),
('wide', True, 'update_time', True),
],
'text': {
'desc': 'Display prices for cryptocurrency and other assets',
'usage': '[opts] [TRADE_SPECIFIER]',
'options': f"""
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-A, --adjust=P Adjust prices by percentage P. In trading mode,
spot and adjusted prices are shown in separate columns.
-b, --btc Fetch and display data for Bitcoin only
-c, --add-columns=LIST Add columns for asset specifiers in LIST (comma-
separated, see ASSET SPECIFIERS below). Can also be
used to supply a USD exchange rate for missing assets.
-C, --cached-data Use cached data from previous network query instead of
live data from server
-d, --cachedir=D Read and write cached JSON data to directory D
instead of ~/{os.path.relpath(cachedir,start=homedir)}
-e, --add-precision=N Add N digits of precision to columns
-E, --elapsed Show elapsed time in UPDATED column (see --update-time)
-F, --portfolio Display portfolio data
-l, --list-ids List IDs of all available assets
-n, --name-labels Label rows with asset names rather than symbols
-p, --percent-change Add percentage change columns
-P, --pager Pipe the output to a pager
-r, --add-rows=LIST Add rows for asset specifiers in LIST (comma-separated,
see ASSET SPECIFIERS below). Can also be used to supply
a USD exchange rate for missing assets.
-T, --thousands-comma Use comma as a thousands separator
-u, --update-time Include UPDATED (last update time) column
-U, --print-curl Print cURL command to standard output and exit
-w, --wide Display all optional columns (equivalent to -punT)
-x, --proxy=P Connect via proxy P. Set to the empty string to
disable. Consult the curl manpage for --proxy usage.
""",
'notes': """
The script has two display modes: overview, the default, and trading, the
latter being enabled when a TRADE_SPECIFIER argument (see below) is supplied
on the command line.
Overview mode displays prices of all configured assets, and optionally the
users portfolio, while trading mode displays the price of a given quantity
of an asset in relation to other assets, optionally comparing an offered
price to the spot price.
ASSETS consist of either a symbol (e.g. xmr) or full ID consisting of
symbol plus label (e.g. xmr-monero). In cases where the symbol is
ambiguous, the full ID must be used. Examples:
chf - specify asset by symbol
chf-swiss-franc-token - same as above, but use full ID instead of symbol
ASSET SPECIFIERS consist of an ASSET followed by an optional colon and USD
rate. If the asset is not in the source data (see --list-ids), the label
part of the ID may be arbitrarily chosen by the user. Examples:
inr:79.5 - INR is not in the source data, so supply USD rate
inr-indian-rupee:79.5 - same as above, but add an arbitrary label
ada-cardano:0.51 - ADA is in the source data, so use the listed ID
A TRADE_SPECIFIER is a single argument in the format:
ASSET:AMOUNT[:TO_ASSET[:TO_AMOUNT]]
Examples:
xmr:17.34 - price of 17.34 XMR in all configured assets
xmr-monero:17.34 - same as above, but with full ID
xmr:17.34:eur - price of 17.34 XMR in EUR only
xmr:17.34:eur:2800 - commission on an offer of 17.34 XMR for 2800 EUR
TO_AMOUNT, if included, is used to calculate the percentage difference or
commission on an offer compared to the spot price.
If either ASSET or TO_ASSET refer to assets not present in the source data,
a USD rate for the missing asset(s) must be supplied via the --add-columns
or --add-rows options.
PROXY NOTE
The remote server used to obtain the price data, {api_host!r}, blocks
Tor behind a Captcha wall, so a Tor proxy cannot be used directly. If youre
concerned about privacy, connect via a VPN, or better yet, VPN over Tor. Then
set up an HTTP proxy (e.g. Privoxy) on the VPNed host and set the proxy
option in the config file or --proxy on the command line accordingly. Or run
the script directly on the VPNed host with proxy or --proxy set to the
null string.
Alternatively, you may download the JSON source data in a Tor-proxied browser
from {api_url}, save it as ticker.json in your
configured cache directory, and run the script with the --cached-data option.
RATE LIMITING NOTE
To protect user privacy, all filtering and processing of data is performed
client side so that the remote server does not know which assets are being
examined. This means that data for ALL available assets (currently over 4000)
is fetched with each invocation of the script. A rate limit of {ratelimit} seconds
between calls is thus imposed to prevent abuse of the remote server. When the
--btc option is in effect, this limit is reduced to 10 seconds. To bypass the
rate limit entirely, use --cached-data.
EXAMPLES
# Basic display in ‘overview’ mode:
$ mmnode-ticker
# Display BTC price only:
$ mmnode-ticker --btc
# Wide display, add EUR and INR columns, INR rate, extra precision and proxy:
$ mmnode-ticker -w -c eur,inr-indian-rupee:79.5 -e2 -x http://vpnhost:8118
# Wide display, use cached data from previous network query, show portfolio
# (see above), pipe output to pager, add DOGE row:
$ mmnode-ticker -wCFP -r doge
# Display 17.234 XMR priced in all configured assets (‘trading’ mode):
$ mmnode-ticker xmr:17.234
# Same as above, but add INR price at specified USDINR rate:
$ mmnode-ticker -c inr:79.5 xmr:17.234
# Same as above, but view INR price only at specified rate, adding label:
$ mmnode-ticker -c inr-indian-rupee:79.5 xmr:17.234:inr
# Calculate commission on an offer of 2700 USD for 0.123 BTC, compared to
# current spot price:
$ mmnode-ticker usd:2700:btc:0.123
# Calculate commission on an offer of 200000 INR for 0.1 BTC, compared to
# current spot price, at specified USDINR rate:
$ mmnode-ticker -n -c inr-indian-rupee:79.5 inr:200000:btc:0.1
CONFIGURED ASSETS:
{assets}
Customize output by editing the file
~/{cfg}
To add a portfolio, edit the file
~/{pf_cfg}
"""
},
'code': {
'notes': lambda s: s.format(
assets = fmt_list(assets_list_gen(cfg_in),fmt='col',indent=' '),
cfg = os.path.relpath(cfg_in.cfg_file,start=homedir),
pf_cfg = os.path.relpath(cfg_in.portfolio_file,start=homedir),
api_host = api_host,
api_url = api_url,
ratelimit = ratelimit,
)
}
}
cmd_args = opts.init(opts_data,do_post_init=True)
cfg_in = get_cfg_in()
cfg = make_cfg(cmd_args,cfg_in)
opts.post_init()
main(cfg,cfg_in)

View file

@ -23,7 +23,7 @@ python_requires = >=3.7
include_package_data = True
install_requires =
mmgen>=13.2.dev10
mmgen>=13.2.dev12
packages =
mmgen_node_tools
@ -36,3 +36,4 @@ scripts =
cmds/mmnode-halving-calculator
cmds/mmnode-netrate
cmds/mmnode-peerblocks
cmds/mmnode-ticker

View file

@ -0,0 +1 @@
{"id":"btc-bitcoin","name":"Bitcoin","symbol":"BTC","rank":"1","price_usd":"23368.859557988893","price_btc":"1","volume_24h_usd":"24116251608.791744","market_cap_usd":"446560795287","circulating_supply":"19109225","total_supply":"19109231","max_supply":"21000000","percent_change_1h":"-0.23","percent_change_24h":"-1.87","percent_change_7d":"6.05","last_updated":"1659346445"}

View file

@ -0,0 +1,24 @@
proxy: http://asdfzxcv:32459
cachedir:
assets:
coin1:
- btc-bitcoin
- ltc-litecoin
- eth-ethereum
- xmr-monero
coin2:
- ada-cardano
- algo-algorand
commodity:
- xau-gold-spot-token
- xag-silver-spot-token
- xbr-brent-crude-oil-spot
fiat:
- chf-swiss-franc-token
- eur-euro-token
index:
- dj30-dow-jones-30-token
- spx-sp-500
- ndx-nasdaq-100-token

View file

@ -0,0 +1,6 @@
btc-bitcoin: '1.23456789'
eth-ethereum: '2.345678901234567890'
xmr-monero: '4.567890123456'
ada-cardano: '123.45678901'
algo-algorand: '234.5678901'
noc-nocoin: '777.1234'

View file

@ -0,0 +1,5 @@
btc-bitcoin: '1.23456789'
eth-ethereum: '2.345678901234567890'
xmr-monero: '4.567890123456'
ada-cardano: '123.45678901'
algo-algorand: '234.5678901'

File diff suppressed because one or more lines are too long

View file

@ -14,6 +14,7 @@ list_avail_tests() {
echo " btc_rt - Bitcoin regtest"
echo " bch_rt - Bitcoin Cash Node (BCH) regtest"
echo " ltc_rt - Litecoin regtest"
echo " scripts - tests of scripts not requiring a coin daemon"
echo " misc - miscellaneous tests that don't fit in the above categories"
echo
echo "AVAILABLE TEST GROUPS:"
@ -27,10 +28,10 @@ list_avail_tests() {
}
init_groups() {
dfl_tests='unit misc btc_rt bch_rt ltc_rt'
dfl_tests='unit misc scripts btc_rt bch_rt ltc_rt'
extra_tests=''
noalt_tests='unit misc btc_rt'
quick_tests='unit misc btc_rt'
noalt_tests='unit misc scripts btc_rt'
quick_tests='unit misc scripts btc_rt'
qskip_tests='bch_rt ltc_rt'
}
@ -45,6 +46,11 @@ init_tests() {
t_misc="- $test_py helpscreens"
f_misc='Misc tests completed'
i_scripts='No-daemon scripts'
s_scripts="The following tests will test scripts not requiring a coin daemon"
t_scripts="- $test_py scripts"
f_scripts='No-daemon script tests completed'
i_btc_rt='Bitcoin regtest'
s_btc_rt="The following tests will test various scripts using regtest mode"
t_btc_rt="- $test_py regtest"

View file

@ -16,6 +16,7 @@ import os
cmd_groups_dfl = {
'helpscreens': ('TestSuiteHelp',{'modname':'misc','full_data':True}),
'scripts': ('TestSuiteScripts',{'modname':'misc'}),
'regtest': ('TestSuiteRegtest',{}),
}
@ -23,6 +24,7 @@ cmd_groups_extra = {}
cfgs = {
'1': {}, # regtest
'2': {}, # scripts
}
def fixup_cfgs(): pass

View file

@ -12,6 +12,7 @@
ts_misc.py: Miscellaneous test groups for the test.py test suite
"""
import shutil
from ..include.common import *
from .common import *
from .ts_base import *
@ -48,3 +49,225 @@ class TestSuiteHelp(TestSuiteBase):
def longhelpscreens(self):
return self.helpscreens(arg='--longhelp',expect='USAGE:.*LONG OPTIONS:')
refdir = os.path.join('test','ref','ticker')
class TestSuiteScripts(TestSuiteBase):
'scripts not requiring a coin daemon'
networks = ('btc',)
tmpdir_nums = [2]
passthru_opts = ()
color = True
cmd_group_in = (
('subgroup.ticker_setup', []),
('subgroup.ticker', ['ticker_setup']),
)
cmd_subgroups = {
'ticker_setup': (
"setup for 'ticker' subgroup",
('ticker_setup', 'ticker setup'),
),
'ticker': (
"'mmnode-ticker' script",
('ticker1', 'ticker [--help)'),
('ticker2', 'ticker (bad proxy)'),
('ticker3', 'ticker [--cached-data]'),
('ticker4', 'ticker [--cached-data --wide]'),
('ticker5', 'ticker [--cached-data --wide --adjust=-0.766] (usr cfg file)'),
('ticker6', 'ticker [--cached-data --wide --portfolio] (missing portfolio)'),
('ticker7', 'ticker [--cached-data --wide --portfolio]'),
('ticker8', 'ticker [--cached-data --wide --elapsed]'),
('ticker9', 'ticker [--cached-data --wide --portfolio --elapsed --add-rows=fake-fakecoin:0.0123 --add-precision=2]'),
('ticker10', 'ticker [--cached-data xmr:17.234]'),
('ticker11', 'ticker [--cached-data xmr:17.234:btc]'),
('ticker12', 'ticker [--cached-data --adjust=1.23 xmr:17.234:btc]'),
('ticker13', 'ticker [--cached-data --wide --elapsed -c inr-indian-rupee:79.5 inr:200000:btc:0.1]'),
('ticker14', 'ticker [--cached-data --wide --btc]'),
('ticker15', 'ticker [--cached-data --wide --btc btc:2:usd:45000]'),
)
}
@property
def ticker_args(self):
return [ f'--cachedir={self.tmpdir}', '--proxy=http://asdfzxcv:32459' ]
@property
def nt_datadir(self):
return os.path.join( g.data_dir_root, 'node_tools' )
def ticker_setup(self):
self.spawn('',msg_only=True)
shutil.copy2(os.path.join(refdir,'ticker.json'),self.tmpdir)
shutil.copy2(os.path.join(refdir,'ticker-btc.json'),self.tmpdir)
return 'ok'
def ticker(self,args=[],expect_list=None,cached=True):
t = self.spawn(
f'mmnode-ticker',
(['--cached-data'] if cached else []) + self.ticker_args + args )
if expect_list:
t.match_expect_list(expect_list)
return t
def ticker1(self):
t = self.ticker(['--help'])
t.expect('USAGE:')
return t
def ticker2(self):
t = self.ticker(cached=False)
if not opt.skipping_deps:
t.expect('Creating')
t.expect('Creating')
t.expect('proxy host could not be resolved')
t.req_exit_val = 3
return t
def ticker3(self):
return self.ticker(
[],
[
'USD BTC',
'BTC 23250.77 1.00000000 ETH 1659.66 0.07146397'
])
def ticker4(self):
return self.ticker(
['--wide','--add-columns=eur,inr-indian-rupee:79.5'],
[
r'EUR \(EURO TOKEN\) = 1.0186 USD ' +
r'INR \(INDIAN RUPEE\) = 0.012579 USD',
'USD EUR INR BTC CHG_7d CHG_24h UPDATED',
'BITCOIN',
r'ETHEREUM 1,659.66 1,629.3906 131,943.14 0.07146397 \+21.42 \+1.82',
r'MONERO 158.97 156.0734 12,638.36 0.00684527 \+7.28 \+1.21 2022-08-02 18:25:59',
r'INDIAN RUPEE 0.01 0.0123 1.00 0.00000054 -- --',
])
def ticker5(self):
shutil.copy2(os.path.join(refdir,'ticker-cfg.yaml'),self.nt_datadir)
t = self.ticker(
['--wide','--adjust=-0.766'],
[
'Adjusting prices by -0.77%',
'USD BTC CHG_7d CHG_24h UPDATED',
r'LITECOIN 58.56 0.00252162 \+12.79 \+0.40 2022-08-02 18:25:59',
r'MONERO 157.76 0.00679284 \+7.28 \+1.21'
])
os.unlink(os.path.join(self.nt_datadir,'ticker-cfg.yaml'))
return t
def ticker6(self):
t = self.ticker( ['--wide','--portfolio'], None )
t.expect('No portfolio')
t.req_exit_val = 1
return t
def ticker7(self): # demo
shutil.copy2(os.path.join(refdir,'ticker-portfolio.yaml'),self.nt_datadir)
t = self.ticker(
['--wide','--portfolio'],
[
'USD BTC CHG_7d CHG_24h UPDATED',
r'ETHEREUM 1,659.66 0.07146397 \+21.42 \+1.82 2022-08-02 18:25:59',
'CARDANO','ALGORAND',
'PORTFOLIO','BITCOIN','ETHEREUM','MONERO','CARDANO','ALGORAND','TOTAL'
])
os.unlink(os.path.join(self.nt_datadir,'ticker-portfolio.yaml'))
return t
def ticker8(self):
return self.ticker(
['--wide','--elapsed'],
[
'USD BTC CHG_7d CHG_24h UPDATED',
r'BITCOIN 23,250.77 1.00000000 \+11.15 \+0.89 10 minutes ago'
])
def ticker9(self):
shutil.copy2(
os.path.join(refdir,'ticker-portfolio-bad.yaml'),
os.path.join(self.nt_datadir,'ticker-portfolio.yaml') )
t = self.ticker(
['--wide','--portfolio','--elapsed','--add-rows=fake-fakecoin:0.0123','--add-precision=2'],
[
'USD BTC CHG_7d CHG_24h UPDATED',
r'BITCOIN 23,250.7741 1.0000000000 \+11.15 \+0.89 10 minutes ago',
r'FAKECOIN 81.3008 0.0034966927 -- -- just now',
r'\(no data for noc-nocoin\)',
])
os.unlink(os.path.join(self.nt_datadir,'ticker-portfolio.yaml'))
return t
def ticker10(self):
return self.ticker(
['XMR:17.234'],
[
r'XMR \(MONERO\) = 158.97 USD ' +
'Amount: 17.234 XMR',
'SPOT PRICE',
'BTC 0.11783441',
'XMR 17.23400000',
'XAU','NDX',
])
def ticker11(self):
return self.ticker(
['XMR:17.234:BTC'],
[
r'XMR \(MONERO\) = 158.97 USD ' +
r'BTC \(BITCOIN\) = 23250.77 USD ' +
'Amount: 17.234 XMR',
'SPOT PRICE',
'XMR 17.23400000 BTC 0.11783441',
])
def ticker12(self):
return self.ticker(
['--adjust=1.23','--wide','XMR:17.234:BTC'],
[
r'XMR \(MONERO\) = 158.97 USD ' +
r'BTC \(BITCOIN\) = 23,250.77 USD ' +
'Amount: 17.234 XMR',
r'Adjusting prices by \+1.23%',
'SPOT PRICE ADJUSTED PRICE',
'MONERO 17.23400000 17.44597820 2022-08-02 18:25:59 ' +
'BITCOIN 0.11783441 0.11928377 2022-08-02 18:25:59',
])
def ticker13(self):
return self.ticker(
['-wE','-c','inr-indian-rupee:79.5','inr:200000:btc:0.1'],
[
'Offer: 200,000 INR',
'Offered price differs from spot by -7.58%',
'SPOT PRICE OFFERED PRICE UPDATED',
'INDIAN RUPEE 200,000.00000000 184,843.65372424 10 minutes ago ' +
'BITCOIN 0.10819955 0.10000000 10 minutes ago'
])
def ticker14(self):
shutil.copy2(os.path.join(refdir,'ticker-portfolio.yaml'),self.nt_datadir)
t = self.ticker(
['--btc','--wide','--portfolio','--elapsed'],
[
'PRICES',
r'BITCOIN 23,368.86 \+6.05 -1.87 33 hours, 2 minutes ago',
'PORTFOLIO',
r'BITCOIN 28,850.44 \+6.05 -1.87 1.23456789'
])
os.unlink(os.path.join(self.nt_datadir,'ticker-portfolio.yaml'))
return t
def ticker15(self):
return self.ticker(
['--btc','--wide','--elapsed','-r','inr:79.5','btc:2:usd:45000'],
[
r'BTC \(BITCOIN\) = 23,368.86 USD',
'Offered price differs from spot by -3.72%',
'SPOT PRICE OFFERED PRICE UPDATED',
'BITCOIN 2.00000000 1.92563954 33 hours, 2 minutes ago ' +
'US DOLLAR 46,737.71911598 45,000.00000000 33 hours, 2 minutes ago',
])