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:
parent
f9a084dba9
commit
e7fcc00b95
17 changed files with 1416 additions and 7 deletions
17
cmds/mmnode-ticker
Executable file
17
cmds/mmnode-ticker
Executable 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
102
mmgen_node_tools/Misc.py
Executable 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
777
mmgen_node_tools/Ticker.py
Executable 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 you’re 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 '' )
|
||||
)
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
30
mmgen_node_tools/data/ticker-cfg.yaml
Normal file
30
mmgen_node_tools/data/ticker-cfg.yaml
Normal 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
|
||||
6
mmgen_node_tools/data/ticker-portfolio.yaml
Normal file
6
mmgen_node_tools/data/ticker-portfolio.yaml
Normal 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'
|
||||
|
|
@ -1 +1 @@
|
|||
3.1.dev3
|
||||
3.1.dev4
|
||||
|
|
|
|||
193
mmgen_node_tools/main_ticker.py
Executable file
193
mmgen_node_tools/main_ticker.py
Executable 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
|
||||
user’s 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 you’re
|
||||
concerned about privacy, connect via a VPN, or better yet, VPN over Tor. Then
|
||||
set up an HTTP proxy (e.g. Privoxy) on the VPN’ed host and set the ‘proxy’
|
||||
option in the config file or --proxy on the command line accordingly. Or run
|
||||
the script directly on the VPN’ed 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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
1
test/ref/ticker/ticker-btc.json
Normal file
1
test/ref/ticker/ticker-btc.json
Normal 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"}
|
||||
24
test/ref/ticker/ticker-cfg.yaml
Normal file
24
test/ref/ticker/ticker-cfg.yaml
Normal 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
|
||||
6
test/ref/ticker/ticker-portfolio-bad.yaml
Normal file
6
test/ref/ticker/ticker-portfolio-bad.yaml
Normal 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'
|
||||
5
test/ref/ticker/ticker-portfolio.yaml
Normal file
5
test/ref/ticker/ticker-portfolio.yaml
Normal 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'
|
||||
1
test/ref/ticker/ticker.json
Normal file
1
test/ref/ticker/ticker.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue