3 Commits e7de689079 ... 060b968ad4

Author SHA1 Message Date
  The MMGen Project 060b968ad4 mmnode-ticker: display crypto assets by market cap 1 month ago
  The MMGen Project 083b29eae8 mmnode-ticker: minor fixes and cleanups 1 month ago
  The MMGen Project 9ffaed6a91 mmnode-ticker: improve options setting 1 month ago
4 changed files with 147 additions and 41 deletions
  1. 86 33
      mmgen_node_tools/Ticker.py
  2. 1 1
      mmgen_node_tools/data/version
  3. 18 7
      mmgen_node_tools/main_ticker.py
  4. 42 0
      test/cmdtest_d/misc.py

+ 86 - 33
mmgen_node_tools/Ticker.py

@@ -25,7 +25,7 @@ from decimal import Decimal
 from collections import namedtuple
 
 from mmgen.color import red, yellow, green, blue, orange, gray
-from mmgen.util import msg, msg_r, rmsg, Msg, Msg_r, die, fmt, fmt_list, fmt_dict, list_gen, suf
+from mmgen.util import msg, msg_r, rmsg, Msg, Msg_r, die, fmt, fmt_list, fmt_dict, list_gen, suf, is_int
 from mmgen.ui import do_pager
 
 homedir = os.getenv('HOME')
@@ -182,9 +182,10 @@ class DataSource:
 		net_data_type = 'json'
 		has_verbose = True
 		dfl_asset_limit = 2000
+		max_asset_idx = 1_000_000
 
 		def __init__(self):
-			self.asset_limit = int(cfg.asset_limit or self.dfl_asset_limit)
+			self.asset_limit = int(cfg.asset_limit) if is_int(cfg.asset_limit) else self.dfl_asset_limit
 
 		def rate_limit_errmsg(self, elapsed):
 			rem = self.timeout - elapsed
@@ -573,9 +574,20 @@ def main():
 		return
 
 	if gcfg.list_ids:
-		do_pager('\n'.join(e.data['id'] for e in src_data['cc']))
+		do_pager('\n'.join(e['id'] for e in src_data['cc'].data))
 		return
 
+	global cfg
+
+	if cfg.asset_range:
+		func = DataSource.coinpaprika.parse_asset_id
+		n, m = cfg.asset_range
+		cfg = cfg._replace(rows =
+			tuple(func(e['id'], require_label=False) for e in src_data['cc'].data[n-1:m])
+			+ tuple(['-'])
+			+ tuple([func('btc-bitcoin', require_label=True)])
+			+ tuple(r for r in cfg.rows if isinstance(r, tuple) and r.source == 'fi'))
+
 	global now
 	now = 1659465400 if gcfg.test_suite else time.time() # 1659524400 1659445900
 
@@ -606,7 +618,7 @@ def make_cfg(gcfg_arg):
 		return tuple(gen())
 
 	def parse_percent_cols(arg):
-		if arg is None:
+		if arg is None or arg.lower() in ('none', ''):
 			return []
 		res = arg.lower().split(',')
 		for s in res:
@@ -638,9 +650,26 @@ def make_cfg(gcfg_arg):
 				source  = parsed_id.source)
 
 		cl_opt = getattr(gcfg, key)
+		if (cl_opt or '').lower() in ('', 'none'):
+			return ()
 		cf_opt = cfg_in.cfg.get(key,[]) if use_cf_file else []
 		return tuple(parse_parm(s) for s in (cl_opt.split(',') if cl_opt else cf_opt))
 
+	def parse_asset_range(s):
+		max_idx = DataSource.coinpaprika.max_asset_idx
+		match s.split('-'):
+			case [a, b] if is_int(a) and is_int(b):
+				n, m = (int(a), int(b))
+			case [a] if is_int(a):
+				n, m = (1, int(a))
+			case _:
+				return None
+		if n < 1 or m < 1 or n > m:
+			raise ValueError(f'‘{s}’: invalid asset range specifier')
+		if m > max_idx:
+			raise ValueError(f'‘{s}’: end of range must be <= {max_idx}')
+		return (n, m)
+
 	def parse_query_arg(s):
 		"""
 		asset_id:amount[:to_asset_id[:to_amount]]
@@ -712,11 +741,24 @@ def make_cfg(gcfg_arg):
 					rows += (hdr,) + uniq_data
 		return rows
 
+	def get_cfg_var(name):
+		if name in gcfg._uopts:
+			return getattr(gcfg, name)
+		else:
+			return getattr(gcfg, name) or cfg_in.cfg.get(name)
+
+	def get_proxy(name):
+		proxy = getattr(gcfg, name)
+		return (
+			'' if proxy == '' else 'none' if (proxy and proxy.lower() == 'none')
+			else (proxy or cfg_in.cfg.get(name)))
+
 	cfg_tuple = namedtuple('global_cfg',[
 		'rows',
 		'usr_rows',
 		'usr_columns',
 		'query',
+		'asset_range',
 		'adjust',
 		'clsname',
 		'btc_only',
@@ -743,7 +785,6 @@ def make_cfg(gcfg_arg):
 	src_cls = {k: getattr(DataSource, v) for k, v in DataSource.get_sources().items()}
 	fi_pat = src_cls['fi'].asset_id_pat
 
-	cmd_args = gcfg._args
 	cfg_in = get_cfg_in()
 
 	if gcfg.test_suite: # required for testing with overlay
@@ -751,15 +792,17 @@ def make_cfg(gcfg_arg):
 		this_mod.src_cls = src_cls
 		this_mod.cfg_in = cfg_in
 
+	if cmd_args := gcfg._args:
+		if len(cmd_args) > 1:
+			die(1, 'Only one command-line argument is allowed')
+		asset_range = parse_asset_range(cmd_args[0])
+		query = None if asset_range else parse_query_arg(cmd_args[0])
+	else:
+		asset_range = None
+		query = None
+
 	usr_rows    = parse_usr_asset_arg('add_rows')
 	usr_columns = parse_usr_asset_arg('add_columns', use_cf_file=True)
-	query       = parse_query_arg(cmd_args[0]) if cmd_args else None
-
-	def get_proxy(name):
-		proxy = getattr(gcfg, name)
-		return (
-			'' if proxy == '' else 'none' if (proxy and proxy.lower() == 'none')
-			else (proxy or cfg_in.cfg.get(name)))
 
 	proxy = get_proxy('proxy')
 	proxy = None if proxy == 'none' else proxy
@@ -770,29 +813,27 @@ def make_cfg(gcfg_arg):
 		usr_rows    = usr_rows,
 		usr_columns = usr_columns,
 		query       = query,
+		asset_range = asset_range,
 		adjust      = (lambda x: (100 + x) / 100 if x else 1)(Decimal(gcfg.adjust or 0)),
 		clsname     = 'trading' if query else 'overview',
-		btc_only    = gcfg.btc or cfg_in.cfg.get('btc'),
-		add_prec    = parse_add_precision(gcfg.add_precision or cfg_in.cfg.get('add_precision')),
-		cachedir    = gcfg.cachedir or cfg_in.cfg.get('cachedir') or dfl_cachedir,
+		btc_only    = get_cfg_var('btc'),
+		add_prec    = parse_add_precision(get_cfg_var('add_precision')),
+		cachedir    = get_cfg_var('cachedir') or dfl_cachedir,
 		proxy       = proxy,
 		proxy2      = None if proxy2 == 'none' else '' if proxy2 == '' else (proxy2 or proxy),
 		portfolio   =
-			get_portfolio()
-				if cfg_in.portfolio
-				and (gcfg.portfolio or cfg_in.cfg.get('portfolio'))
-				and not query
+			get_portfolio() if cfg_in.portfolio and get_cfg_var('portfolio') and not query
 			else None,
-		percent_cols    = parse_percent_cols(gcfg.percent_cols or cfg_in.cfg.get('percent_cols')),
-		asset_limit     = gcfg.asset_limit     or cfg_in.cfg.get('asset_limit'),
-		cached_data     = gcfg.cached_data     or cfg_in.cfg.get('cached_data'),
-		elapsed         = gcfg.elapsed         or cfg_in.cfg.get('elapsed'),
-		name_labels     = gcfg.name_labels     or cfg_in.cfg.get('name_labels'),
-		pager           = gcfg.pager           or cfg_in.cfg.get('pager'),
-		thousands_comma = gcfg.thousands_comma or cfg_in.cfg.get('thousands_comma'),
-		update_time     = gcfg.update_time     or cfg_in.cfg.get('update_time'),
-		quiet           = gcfg.quiet           or cfg_in.cfg.get('quiet'),
-		verbose         = gcfg.verbose         or cfg_in.cfg.get('verbose'))
+		percent_cols    = parse_percent_cols(get_cfg_var('percent_cols')),
+		asset_limit     = get_cfg_var('asset_limit'),
+		cached_data     = get_cfg_var('cached_data'),
+		elapsed         = get_cfg_var('elapsed'),
+		name_labels     = get_cfg_var('name_labels'),
+		pager           = get_cfg_var('pager'),
+		thousands_comma = get_cfg_var('thousands_comma'),
+		update_time     = get_cfg_var('update_time'),
+		quiet           = get_cfg_var('quiet'),
+		verbose         = get_cfg_var('verbose'))
 
 def get_cfg_in():
 	ret = namedtuple('cfg_in_data', ['cfg', 'portfolio', 'cfg_file', 'portfolio_file'])
@@ -920,12 +961,14 @@ class Ticker:
 			if self.table_hdr:
 				yield self.table_hdr
 
-			for row in self.rows:
+			for n, row in enumerate(self.rows, cfg.asset_range[0] if cfg.asset_range else 1):
 				if isinstance(row, str):
+					if cfg.asset_range:
+						return
 					yield ('-' * self.hl_wid)
 				else:
 					try:
-						yield self.fmt_row(self.data[row.id])
+						yield self.fmt_row(self.data[row.id], idx=n)
 					except KeyError:
 						yield gray(f'(no data for {row.id})')
 
@@ -979,7 +1022,7 @@ class Ticker:
 						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_row(self, d, amt=None, amt_fmt=None, idx=None):
 
 			def fmt_pct(n):
 				return gray('     --') if n is None else (red, green)[n>=0](f'{n:+7.2f}')
@@ -992,6 +1035,7 @@ class Ticker:
 					amt_fmt = amt_fmt.rstrip('0').rstrip('.')
 
 			return self.fs_num.format(
+				idx = idx,
 				lbl = self.create_label(d['id']) if cfg.name_labels else d['symbol'],
 				pc1 = fmt_pct(d.get('percent_change_7d')),
 				pc2 = fmt_pct(d.get('percent_change_24h')),
@@ -1051,6 +1095,15 @@ class Ticker:
 			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)
 
+			if cfg.asset_range:
+				def get_col1_w():
+					for n, r in enumerate(cfg.rows):
+						if isinstance(r, str):
+							return len(str(n))
+				col1_w = get_col1_w()
+				self.fs_str = ' ' * (col1_w + 2) + self.fs_str
+				self.fs_num = f'{{idx:{col1_w}}}) ' + self.fs_num
+
 		@property
 		def table_hdr(self):
 			return self.fs_str.format(
@@ -1126,7 +1179,7 @@ class Ticker:
 				self.fs_str += '  {upd}'
 				self.hl_wid += self.upd_w + 2
 
-		def fmt_row(self, d):
+		def fmt_row(self, d, idx=None):
 			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)

+ 1 - 1
mmgen_node_tools/data/version

@@ -1 +1 @@
-3.6.dev4
+3.6.dev5

+ 18 - 7
mmgen_node_tools/main_ticker.py

@@ -25,7 +25,7 @@ opts_data = {
 	],
 	'text': {
 		'desc':  'Display prices for cryptocurrency and other assets',
-		'usage': '[opts] [TRADE_SPECIFIER]',
+		'usage': '[opts] [TRADE_SPECIFIER | ASSET_RANGE]',
 		'options': """
 -h, --help            Print this help message
 --, --longhelp        Print help message for long options (common options)
@@ -71,14 +71,19 @@ opts_data = {
 """,
 	'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.
+The script has three display modes: ‘overview’, enabled when no arguments are
+given on the command line; ‘trading’, when a TRADE_SPECIFIER argument (see
+below) is given; and ‘market cap’, when an ASSET_RANGE (see below) is given.
 
 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.
+user’s portfolio; 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; and market cap mode lists a range of crypto assets selected
+by current market cap.
+
+The ASSET_RANGE argument can be either an integer N, in which case the top
+N assets by market cap will be displayed, or a hyphen-separated range N-M,
+in which case assets from N to M by market cap will be displayed.
 
 ASSETS consist of either a symbol (e.g. ‘xmr’) or full ID (see --list-ids)
 consisting of symbol plus label (e.g. ‘xmr-monero’).  In cases where the
@@ -199,6 +204,12 @@ $ mmnode-ticker usd:2700:btc:0.123
 # current spot price, at specified USDINR rate:
 $ mmnode-ticker -n -c inr-indian-rupee:79.5 inr:200000:btc:0.1
 
+# Display top 20 crypto assets by market cap, adding a Euro column:
+$ mmnode-ticker -c eurusd=x 20
+
+# Same as above, specifying assets using a range:
+$ mmnode-ticker -c eurusd=x 1-20
+
 
 CONFIGURED ASSETS:
 {assets}

+ 42 - 0
test/cmdtest_d/misc.py

@@ -86,6 +86,10 @@ class CmdTestScripts(CmdTestBase):
 		('ticker15', 'ticker [--cached-data --wide --btc btc:2:usd:45000]'),
 		('ticker16', 'ticker [--cached-data --wide --elapsed -c eur,omr-omani-rial:2.59r'),
 		('ticker17', 'ticker [--cached-data --wide --elapsed -c bgn-bulgarian-lev:0.5113r:eur'),
+		('ticker18', 'ticker [--cached-data --widest --add-columns eurusd=x 10]'),
+		('ticker19', 'ticker [--cached-data 1-5]'),
+		('ticker20', 'ticker [--cached-data 2-5]'),
+		('ticker21', 'ticker [--cached-data 5-5]'),
 	)
 	}
 
@@ -319,3 +323,41 @@ class CmdTestScripts(CmdTestBase):
 				'BITCOIN 23,250.77 42,731.767 1.00000000',
 				'BULGARIAN LEV 0.54 1.000 0.00002340',
 			])
+
+	def ticker18(self):
+		return self.ticker(
+			['10'],
+			[
+				r'1\) BITCOIN 23,250.77 21,848.7527 1.00000000 \+18.96 \+15.61 \+11.15 \+0.89',
+				r'6\) ALGORAND 0.33 0.3120 0.00001428 \+16.47 \+13.57 \+9.69 \-0.82'
+			],
+			add_opts = ['--widest', '--add-columns=eurusd=x'])
+
+	def ticker19(self):
+		return self.ticker(
+			['1-5'],
+			[
+				'USD EURUSD=X BTC '
+				r'1\) BTC 23250.77 21848.7527 1.00000000',
+				r'5\) ADA 0.51 0.4764 0.00002180',
+			],
+			add_opts = ['--add-columns=eurusd=x'])
+
+	def ticker20(self):
+		return self.ticker(
+			['2-5'],
+			[
+				'USD EURUSD=X BTC '
+				r'2\) ETH 1659.66 1559.5846 0.07138094',
+				r'5\) ADA 0.51 0.4764 0.00002180',
+			],
+			add_opts = ['--add-columns=eurusd=x'])
+
+	def ticker21(self):
+		return self.ticker(
+			['5-5'],
+			[
+				'USD EURUSD=X BTC '
+				r'5\) ADA 0.51 0.4764 0.00002180',
+			],
+			add_opts = ['--add-columns=eurusd=x'])