3 Commits 9aa4b4dcfe ... 0a953e3ca0

Author SHA1 Message Date
  The MMGen Project 0a953e3ca0 mmnode-ticker: sort output by various parameters 1 month ago
  The MMGen Project e6d62fd18b mmnode-ticker: display coin ranking in first column 1 month ago
  The MMGen Project e71ef141bf mmnode-ticker: minor cleanups 1 month ago
4 changed files with 159 additions and 36 deletions
  1. 77 26
      mmgen_node_tools/Ticker.py
  2. 1 1
      mmgen_node_tools/data/version
  3. 11 2
      mmgen_node_tools/main_ticker.py
  4. 70 7
      test/cmdtest_d/misc.py

+ 77 - 26
mmgen_node_tools/Ticker.py

@@ -24,7 +24,7 @@ from subprocess import run, PIPE, CalledProcessError
 from decimal import Decimal
 from collections import namedtuple
 
-from mmgen.color import red, yellow, green, blue, orange, gray
+from mmgen.color import red, yellow, green, blue, orange, gray, cyan, pink
 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
 
@@ -39,8 +39,16 @@ percent_cols = {
 	'd': 'day',
 	'w': 'week',
 	'm': 'month',
-	'y': 'year',
-}
+	'y': 'year'}
+
+sp = namedtuple('sort_parameter', ['key', 'desc'])
+sort_params = {
+	'd': sp('percent_change_24h', '1-day % change'),
+	'w': sp('percent_change_7d',  '1-week % change'),
+	'm': sp('percent_change_30d', '1-month % change'),
+	'y': sp('percent_change_1y',  '1-year % change'),
+	'p': sp('price_usd',          'asset price'),
+	'c': sp('market_cap',         'market cap')}
 
 class RowDict(dict):
 
@@ -275,6 +283,7 @@ class DataSource:
 				'percent_change_30d': data['pct_chg_4wks'],
 				'percent_change_7d': data['pct_chg_1wk'],
 				'percent_change_24h': data['regularMarketChangePercent']['raw'] * 100,
+				'market_cap': 0, # dummy - required for sorting
 				'last_updated': data['regularMarketTime']}
 
 		def rate_limit_errmsg(self, elapsed):
@@ -713,8 +722,8 @@ def make_cfg(gcfg_arg):
 			return ()
 
 	def get_portfolio():
-		return {k: Decimal(v) for k, v in cfg_in.portfolio.items()
-			if (not gcfg.btc) or k == 'btc-bitcoin'}
+		return tuple((k, Decimal(v)) for k, v in cfg_in.portfolio.items()
+			if (not gcfg.btc) or k == 'btc-bitcoin')
 
 	def parse_add_precision(arg):
 		if not arg:
@@ -753,6 +762,19 @@ def make_cfg(gcfg_arg):
 			'' if proxy == '' else 'none' if (proxy and proxy.lower() == 'none')
 			else (proxy or cfg_in.cfg.get(name)))
 
+	def get_sort_opt():
+		match get_cfg_var('sort'):
+			case None:
+				return None
+			case s if s in sort_params:
+				return (s, True)
+			case s if s in ['r' + ch for ch in sort_params]:
+				return (s[1], False)
+			case s:
+				die(1,
+					f'{s!r}: invalid parameter for --sort option (must be one of {fmt_list(sort_params)})'
+					'\nTo reverse the sort, prefix the code letter with ‘r’')
+
 	cfg_tuple = namedtuple('global_cfg',[
 		'rows',
 		'usr_rows',
@@ -767,6 +789,7 @@ def make_cfg(gcfg_arg):
 		'proxy',
 		'proxy2',
 		'portfolio',
+		'sort',
 		'percent_cols',
 		'asset_limit',
 		'cached_data',
@@ -824,6 +847,7 @@ def make_cfg(gcfg_arg):
 		proxy       = proxy,
 		proxy2      = None if proxy2 == 'none' else '' if proxy2 == '' else (proxy2 or proxy),
 		portfolio   = portfolio,
+		sort        = get_sort_opt(),
 		percent_cols    = parse_percent_cols(get_cfg_var('percent_cols')),
 		asset_limit     = get_cfg_var('asset_limit'),
 		cached_data     = get_cfg_var('cached_data'),
@@ -866,9 +890,12 @@ class Ticker:
 
 		offer = None
 		to_asset = None
+		hidden_groups = ('extra',)
 
 		def __init__(self, data):
 
+			global cfg
+
 			self.comma = ',' if cfg.thousands_comma else ''
 
 			self.col1_wid = max(len('TOTAL'), (
@@ -877,6 +904,26 @@ class Ticker:
 
 			self.rows = RowDict(
 				{k: tuple(row._replace(id=self.get_id(row)) for row in v) for k, v in cfg.rows.items()})
+
+			if cfg.asset_range:
+				self.max_rank = 0
+				for group, rows in self.rows.items():
+					if group not in self.hidden_groups:
+						for row in rows:
+							self.max_rank = max(self.max_rank, int(data[row.id]['rank']))
+
+			if cfg.sort:
+				code, reverse = cfg.sort
+				key = sort_params[code].key
+				sort_func    = lambda row: data[row.id][key]
+				pf_sort_func = lambda row: data[row[0]][key]
+				for group in self.rows.keys():
+					if group not in self.hidden_groups:
+						self.rows[group] = sorted(self.rows[group], key=sort_func, reverse=reverse)
+				if cfg.portfolio:
+					cfg = cfg._replace(
+						portfolio = sorted(cfg.portfolio, key=pf_sort_func, reverse=reverse))
+
 			self.col_usd_prices = {k: 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 row.id in data}
 			self.prices['usd-us-dollar'] = self.get_row_prices('usd-us-dollar')
@@ -932,7 +979,20 @@ class Ticker:
 			return self.data[id]['name'].upper()
 
 		def gen_output(self):
-			yield 'Current time: {} UTC'.format(time.strftime('%F %X', time.gmtime(now)))
+
+			def process_rows(rows):
+				yield '-' * self.hl_wid
+				for row in rows:
+					try:
+						yield self.fmt_row(self.data[row.id])
+					except KeyError:
+						yield gray(f'(no data for {row.id})')
+
+			yield 'Current time: {}'.format(cyan(time.strftime('%F %X', time.gmtime(now)) + ' UTC'))
+
+			if cfg.sort:
+				text = sort_params[cfg.sort[0]].desc + ('' if cfg.sort[1] else ' [reversed]')
+				yield f'Sort order: {pink(text.upper())}'
 
 			for asset in self.usr_col_assets:
 				if asset.symbol != 'USD':
@@ -962,21 +1022,11 @@ class Ticker:
 				yield self.table_hdr
 
 			if cfg.asset_range:
-				yield '-' * self.hl_wid
-				for n, row in enumerate(self.rows['asset_list'], cfg.asset_range[0]):
-					try:
-						yield self.fmt_row(self.data[row.id], idx=n)
-					except KeyError:
-						yield gray(f'(no data for {row.id})')
+				yield from process_rows(self.rows['asset_list'])
 			else:
 				for rows in self.rows.values():
 					if rows:
-						yield '-' * self.hl_wid
-						for row in rows:
-							try:
-								yield self.fmt_row(self.data[row.id])
-							except KeyError:
-								yield gray(f'(no data for {row.id})')
+						yield from process_rows(rows)
 
 			yield '-' * self.hl_wid
 
@@ -987,7 +1037,7 @@ class Ticker:
 				yield blue('PORTFOLIO')
 				yield self.table_hdr
 				yield '-' * self.hl_wid
-				for sym, amt in cfg.portfolio.items():
+				for sym, amt in cfg.portfolio:
 					try:
 						yield self.fmt_row(self.data[sym], amt=amt)
 					except KeyError:
@@ -1005,16 +1055,17 @@ class Ticker:
 			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',)
+			self.col_ids = ('usd-us-dollar', 'btc-bitcoin') + tuple(a.id for a in self.usr_col_assets)
 
 			super().__init__(data)
 
 			self.format_last_updated_col()
 
 			if cfg.portfolio:
-				self.prices['total'] = {col_id: sum(self.prices[row.id][col_id] * cfg.portfolio[row.id]
+				pf_dict = dict(cfg.portfolio)
+				self.prices['total'] = {col_id: sum(self.prices[row.id][col_id] * pf_dict[row.id]
 					for row in self.rows
-						if row.id in cfg.portfolio and row.id in data)
+						if row.id in pf_dict and row.id in data)
 							for col_id in self.col_ids}
 
 			self.init_prec()
@@ -1028,7 +1079,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, idx=None):
+		def fmt_row(self, d, amt=None, amt_fmt=None):
 
 			def fmt_pct(n):
 				return gray('     --') if n is None else (red, green)[n>=0](f'{n:+7.2f}')
@@ -1041,7 +1092,7 @@ class Ticker:
 					amt_fmt = amt_fmt.rstrip('0').rstrip('.')
 
 			return self.fs_num.format(
-				idx = idx,
+				idx = int(d['rank']) if cfg.asset_range else None,
 				mcap = d.get('market_cap') / 1_000_000_000 if cfg.asset_range else None,
 				lbl = self.create_label(d['id']) if cfg.name_labels else d['symbol'],
 				pc1 = fmt_pct(d.get('percent_change_7d')),
@@ -1091,7 +1142,7 @@ class Ticker:
 						if b])
 
 			if cfg.asset_range:
-				num_w = len(str(len(cfg.rows['asset_list'])))
+				num_w = len(str(self.max_rank))
 				col_fs_data.update({
 					'idx': fd(' ' * (num_w + 2), f'{{idx:{num_w}}}) ', num_w + 2),
 					'mcap': fd('{mcap:>12}', '{mcap:12.5f}', 12)})
@@ -1186,7 +1237,7 @@ class Ticker:
 				self.fs_str += '  {upd}'
 				self.hl_wid += self.upd_w + 2
 
-		def fmt_row(self, d, idx=None):
+		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)

+ 1 - 1
mmgen_node_tools/data/version

@@ -1 +1 @@
-3.6.dev6
+3.6.dev7

+ 11 - 2
mmgen_node_tools/main_ticker.py

@@ -57,6 +57,9 @@ opts_data = {
 -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.
+-s, --sort=P          Sort output according to parameter P.  Valid parameters
+                      are {sp_codes}. See SORT PARAMETERS below.
+                      To reverse the sort, prefix the parameter with ‘r’.
 -t, --testing         Print command(s) to be executed to stdout and exit
 -T, --thousands-comma Use comma as a thousands separator
 -u, --update-time     Include UPDATED (last update time) column
@@ -133,6 +136,10 @@ A TRADE_SPECIFIER is a single argument in the format:
   a USD rate for the missing asset(s) must be supplied via the --add-columns
   or --add-rows options.
 
+SORT PARAMETERS:
+
+  {sp_fmt}
+
 
                                  PROXY NOTE
 
@@ -225,13 +232,15 @@ To add a portfolio, edit the file
 			dfl_cachedir = os.path.relpath(dfl_cachedir, start=homedir),
 			ds           = fmt_dict(DataSource.get_sources(), fmt='equal_compact'),
 			al           = DataSource.coinpaprika.dfl_asset_limit,
-			pc           = fmt_list(Ticker.percent_cols, fmt='bare')),
+			sp_codes     = fmt_list(sort_params, fmt='fancy'),
+			pc           = fmt_list(Ticker.percent_cols, fmt='fancy')),
 		'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),
 			al     = DataSource.coinpaprika.dfl_asset_limit,
 			cc     = src_cls['cc'](),
+			sp_fmt = '\n  '.join(f'‘{k}’ - {v.desc}' for k, v in sort_params.items()),
 			fi     = src_cls['fi']())
 	}
 }
@@ -246,7 +255,7 @@ gcfg = Config(opts_data=opts_data, caller_post_init=True)
 
 src_cls, cfg_in = Ticker.make_cfg(gcfg)
 
-from .Ticker import dfl_cachedir, homedir, DataSource, assets_list_gen
+from .Ticker import dfl_cachedir, homedir, DataSource, assets_list_gen, sort_params
 
 gcfg._post_init()
 

+ 70 - 7
test/cmdtest_d/misc.py

@@ -90,6 +90,14 @@ class CmdTestScripts(CmdTestBase):
 		('ticker19', 'ticker [--cached-data 1-5]'),
 		('ticker20', 'ticker [--cached-data 2-5]'),
 		('ticker21', 'ticker [--cached-data 5-5]'),
+		('ticker22', 'ticker [--sort=rp]'),
+		('ticker23', 'ticker [--sort=rp xmr:10]'),
+		('ticker24', 'ticker [--sort=p]'),
+		('ticker25', 'ticker [--sort=p 200]'),
+		('ticker26', 'ticker [--sort=c -r algo,ada]'),
+		('ticker27', 'ticker [--sort=rp -r algo,ada]'),
+		('ticker28', 'ticker [--sort=d -r algo,ada]'),
+		('ticker29', 'ticker [--sort=y -r algo,ada]'),
 	)
 	}
 
@@ -329,7 +337,7 @@ class CmdTestScripts(CmdTestBase):
 			['10'],
 			[
 				r'1\) BITCOIN 444.33652 23,250.77 21,848.7527 1.00000000 \+18.96 \+15.61 \+11.15 \+0.89',
-				r'6\) ALGORAND 2.30691 0.33 0.3120 0.00001428 \+16.47 \+13.57 \+9.69 \-0.82'
+				r'33\) ALGORAND 2.30691 0.33 0.3120 0.00001428 \+16.47 \+13.57 \+9.69 \-0.82'
 			],
 			add_opts = ['--widest', '--add-columns=eurusd=x'])
 
@@ -338,10 +346,10 @@ class CmdTestScripts(CmdTestBase):
 			['1-5'],
 			[
 				r'MarketCap\(B\) USD EURUSD=X BTC '
-				'--------------------------------------------------------- '
+				'---------------------------------------------------------- '
 				r'1\) BTC 444.33652 23250.77 21848.7527 1.00000000',
-				r'5\) ADA 17.11161 0.51 0.4764 0.00002180'
-				' ---------------------------------------------------------'
+				r'8\) ADA 17.11161 0.51 0.4764 0.00002180'
+				' ----------------------------------------------------------'
 			],
 			add_opts = ['--add-columns=eurusd=x'])
 
@@ -350,9 +358,9 @@ class CmdTestScripts(CmdTestBase):
 			['2-5'],
 			[
 				r'MarketCap\(B\) USD EURUSD=X BTC '
-				'--------------------------------------------------------- '
+				'---------------------------------------------------------- '
 				r'2\) ETH 202.15129 1659.66 1559.5846 0.07138094',
-				r'5\) ADA 17.11161 0.51 0.4764 0.00002180',
+				r'8\) ADA 17.11161 0.51 0.4764 0.00002180',
 			],
 			add_opts = ['--add-columns=eurusd=x'])
 
@@ -362,6 +370,61 @@ class CmdTestScripts(CmdTestBase):
 			[
 				r'MarketCap\(B\) USD EURUSD=X BTC '
 				'--------------------------------------------------------- '
-				r'5\) ADA 17.11161 0.51 0.4764 0.00002180',
+				r'8\) ADA 17.11161 0.51 0.4764 0.00002180',
 			],
 			add_opts = ['--add-columns=eurusd=x'])
+
+	def ticker22(self):
+		return self.ticker(
+			[],
+			['MONERO', 'ETHEREUM', 'BITCOIN', 'SILVER', 'BRENT', 'GOLD'],
+			add_opts = ['--name-labels', '--sort=rp'])
+
+	def ticker23(self):
+		return self.ticker(
+			[],
+			['MONERO', 'ETHEREUM', 'BITCOIN', 'SILVER', 'BRENT', 'GOLD'],
+			add_opts = ['--name-labels', '--sort=rp', 'xmr:10'])
+
+	def ticker24(self):
+		return self.ticker(
+			[],
+			['BITCOIN', 'ETHEREUM', 'MONERO', 'GOLD', 'BRENT', 'SILVER'],
+			add_opts = ['--name-labels', '--sort=p'])
+
+	def ticker25(self):
+		return self.ticker(
+			[],
+			[
+				r' 1\) BITCOIN',
+				r' 2\) ETHEREUM',
+				r'30\) MONERO',
+				r'23\) LITECOIN',
+				r' 8\) CARDANO',
+				r'33\) ALGORAND'
+			],
+			add_opts = ['--name-labels', '--sort=p', '200'])
+
+	def ticker26(self):
+		return self.ticker(
+			[],
+			['BITCOIN', 'ETHEREUM', 'MONERO', 'CARDANO', 'ALGORAND'],
+			add_opts = ['--name-labels', '--sort=c', '-r', 'ada,algo'])
+
+	def ticker27(self):
+		return self.ticker(
+			[],
+			['MONERO', 'ETHEREUM', 'BITCOIN', 'S&P', 'NASDAQ', 'DOW', 'ALGORAND', 'CARDANO'],
+			add_opts = ['--name-labels', '--sort=rp', '--add-rows=ada-cardano,algo-algorand'])
+
+	def ticker28(self):
+		return self.ticker(
+			[],
+			['ETHEREUM', 'MONERO', 'BITCOIN', 'NASDAQ', 'S&P', 'DOW', 'CARDANO', 'ALGORAND'],
+			add_opts = ['--widest', '--sort=d', '-r', 'ada,algo'])
+
+	def ticker29(self):
+		return self.ticker(
+			[],
+			['ETHEREUM', 'BITCOIN', 'MONERO', 'S&P', 'DOW', 'NASDAQ', 'CARDANO', 'ALGORAND'],
+			add_opts = ['--widest', '-s', 'y', '-r', 'ada,algo'])