4 Commits 060b968ad4 ... 9aa4b4dcfe

Author SHA1 Message Date
  The MMGen Project 9aa4b4dcfe mmnode-ticker: add `MarketCap` column 1 month ago
  The MMGen Project 253aa14a26 Ticker.py: new `RowDict` class 1 month ago
  The MMGen Project 3f921d333c Ticker.py: `parse_asset_id()`: unify call signature 1 month ago
  The MMGen Project c1f42fc25b mmnode-ticker: various fixes and cleanups 1 month ago
4 changed files with 117 additions and 106 deletions
  1. 100 93
      mmgen_node_tools/Ticker.py
  2. 1 1
      mmgen_node_tools/data/version
  3. 2 2
      mmgen_node_tools/main_ticker.py
  4. 14 10
      test/cmdtest_d/misc.py

+ 100 - 93
mmgen_node_tools/Ticker.py

@@ -42,6 +42,11 @@ percent_cols = {
 	'y': 'year',
 }
 
+class RowDict(dict):
+
+	def __iter__(self):
+		return (e for v in self.values() for e in v)
+
 class DataSource:
 
 	source_groups = [
@@ -233,7 +238,7 @@ class DataSource:
 			return [data] if cfg.btc_only else data
 
 		@staticmethod
-		def parse_asset_id(s, require_label):
+		def parse_asset_id(s, require_label=True):
 			sym, label = (*s.split('-', 1), None)[:2]
 			if require_label and not label:
 				die(1, f'{s!r}: asset label is missing')
@@ -286,7 +291,7 @@ class DataSource:
 
 		@property
 		def symbols(self):
-			return [r.symbol for r in cfg.rows if isinstance(r, tuple) and r.source == 'fi']
+			return [r.symbol for r in cfg.rows if r.source == 'fi']
 
 		def get_data_from_network(self):
 
@@ -314,7 +319,7 @@ class DataSource:
 			return ticker.price
 
 		@staticmethod
-		def parse_asset_id(s, require_label):
+		def parse_asset_id(s, require_label=True):
 			return asset_tuple(
 				symbol = s.upper(),
 				id     = s.lower(),
@@ -411,6 +416,7 @@ def gen_data(data):
 								d['percent_change_7d']  = d['quotes']['USD']['percent_change_7d']
 								d['percent_change_30d'] = d['quotes']['USD']['percent_change_30d']
 								d['percent_change_1y']  = d['quotes']['USD']['percent_change_1y']
+								d['market_cap']  = d['quotes']['USD']['market_cap']
 								d['last_updated'] = int(datetime.datetime.fromisoformat(
 									d['last_updated']).timestamp())
 							yield (d['id'], d)
@@ -456,8 +462,8 @@ def gen_data(data):
 			return ()
 
 	rows_want = {
-		'id': {r.id for r in cfg.rows if isinstance(r, tuple) and r.id} - {'usd-us-dollar'},
-		'symbol': {r.symbol for r in cfg.rows if isinstance(r, tuple) and r.id is None} - {'USD'}}
+		'id': {r.id for r in cfg.rows if r.id} - {'usd-us-dollar'},
+		'symbol': {r.symbol for r in cfg.rows if r.id is None} - {'USD'}}
 	usr_rate_assets = tuple(u.rate_asset for u in cfg.usr_rows + cfg.usr_columns if u.rate_asset)
 	usr_rate_assets_want = {
 		'id':     {a.id for a in usr_rate_assets if a.id},
@@ -580,13 +586,16 @@ def main():
 	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'))
+		cfg = cfg._replace(rows = RowDict({
+			'asset_list':
+				tuple(
+					asset_tuple(e['symbol'], e['id'], source='cc')
+						for e in src_data['cc'].data[n-1:m]),
+			'extra':
+				tuple(
+					[asset_tuple('BTC', 'btc-bitcoin', source='cc')]
+					+ [r for r in cfg.rows if r.source == 'fi'])}))
 
 	global now
 	now = 1659465400 if gcfg.test_suite else time.time() # 1659524400 1659445900
@@ -604,19 +613,9 @@ def make_cfg(gcfg_arg):
 	query_tuple = namedtuple('query', ['asset', 'to_asset'])
 	asset_data  = namedtuple('asset_data', ['symbol', 'id', 'amount', 'rate', 'rate_asset', 'source'])
 
-	def parse_asset_id(s, require_label=False):
+	def parse_asset_id(s, require_label=True):
 		return src_cls['fi' if re.match(fi_pat, s) else 'cc'].parse_asset_id(s, require_label)
 
-	def get_rows_from_cfg(add_data=None):
-		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 parse_asset_id(e, require_label=True)
-		return tuple(gen())
-
 	def parse_percent_cols(arg):
 		if arg is None or arg.lower() in ('none', ''):
 			return []
@@ -636,7 +635,7 @@ def make_cfg(gcfg_arg):
 			ss = s.split(':')
 			assert len(ss) in (1, 2, 3), f'{s}: malformed argument'
 			asset_id, rate, rate_asset = (*ss, None, None)[:3]
-			parsed_id = parse_asset_id(asset_id)
+			parsed_id = parse_asset_id(asset_id, require_label=False)
 
 			return asset_data(
 				symbol = parsed_id.symbol,
@@ -646,11 +645,11 @@ def make_cfg(gcfg_arg):
 					None if rate is None else
 					1 / Decimal(rate[:-1]) if rate.lower().endswith('r') else
 					Decimal(rate)),
-				rate_asset = parse_asset_id(rate_asset) if rate_asset else None,
+				rate_asset = parse_asset_id(rate_asset, require_label=False) if rate_asset else None,
 				source  = parsed_id.source)
 
 		cl_opt = getattr(gcfg, key)
-		if (cl_opt or '').lower() in ('', 'none'):
+		if cl_opt is None or cl_opt.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))
@@ -675,7 +674,7 @@ def make_cfg(gcfg_arg):
 		asset_id:amount[:to_asset_id[:to_amount]]
 		"""
 		def parse_query_asset(asset_id, amount):
-			parsed_id = parse_asset_id(asset_id)
+			parsed_id = parse_asset_id(asset_id, require_label=False)
 			return asset_data(
 				symbol = parsed_id.symbol,
 				id     = parsed_id.id,
@@ -702,15 +701,16 @@ def make_cfg(gcfg_arg):
 
 	def get_usr_assets():
 		return (
-			'user_added',
-			usr_rows +
-			(tuple(asset for asset in query if asset) if query else ()) +
-			usr_columns)
+			usr_rows
+			+ (tuple(asset for asset in query if asset) if query else ())
+			+ usr_columns)
 
-	def get_portfolio_assets(ret=()):
+	def get_portfolio_assets():
 		if cfg_in.portfolio and gcfg.portfolio:
-			ret = (parse_asset_id(e, require_label=True) for e in cfg_in.portfolio)
-		return ('portfolio', tuple(e for e in ret if (not gcfg.btc) or e.symbol == 'BTC'))
+			ret = (parse_asset_id(e) for e in cfg_in.portfolio)
+			return tuple(e for e in ret if (not gcfg.btc) or e.symbol == 'BTC')
+		else:
+			return ()
 
 	def get_portfolio():
 		return {k: Decimal(v) for k, v in cfg_in.portfolio.items()
@@ -727,18 +727,18 @@ def make_cfg(gcfg_arg):
 		return int(s)
 
 	def create_rows():
-		rows = (
-			('trade_pair',) + query if (query and query.to_asset) else
-			('bitcoin', parse_asset_id('btc-bitcoin')) if gcfg.btc else
-			get_rows_from_cfg(add_data={'fiat':['usd-us-dollar']} if gcfg.add_columns else None))
-
+		rows = RowDict(
+			{'trade_pair': query} if (query and query.to_asset) else
+			{'bitcoin': [parse_asset_id('btc-bitcoin')]} if gcfg.btc else
+			{k: tuple(parse_asset_id(e) for e in v) for k, v in cfg_in.cfg['assets'].items()})
 		for hdr, data in (
-				(get_usr_assets(),) if query else
-				(get_usr_assets(), get_portfolio_assets())):
+				('user_uniq', get_usr_assets()),
+				('portfolio_uniq', get_portfolio_assets())):
 			if data:
-				uniq_data = tuple(gen_uniq(data, 'symbol', preload=rows))
-				if uniq_data:
-					rows += (hdr,) + uniq_data
+				if uniq_data := tuple(gen_uniq(data, 'symbol', preload=rows)):
+					rows[hdr] = uniq_data
+				else:
+					rows[hdr] = ()
 		return rows
 
 	def get_cfg_var(name):
@@ -787,11 +787,6 @@ def make_cfg(gcfg_arg):
 
 	cfg_in = get_cfg_in()
 
-	if gcfg.test_suite: # required for testing with overlay
-		from . import Ticker as this_mod
-		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')
@@ -808,6 +803,13 @@ def make_cfg(gcfg_arg):
 	proxy = None if proxy == 'none' else proxy
 	proxy2 = get_proxy('proxy2')
 
+	portfolio = (
+		get_portfolio() if cfg_in.portfolio and get_cfg_var('portfolio') and not query
+		else None)
+
+	if portfolio and asset_range:
+		die(1, '--portfolio not supported in market cap view')
+
 	cfg = cfg_tuple(
 		rows        = create_rows(),
 		usr_rows    = usr_rows,
@@ -821,9 +823,7 @@ def make_cfg(gcfg_arg):
 		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 get_cfg_var('portfolio') and not query
-			else None,
+		portfolio   = portfolio,
 		percent_cols    = parse_percent_cols(get_cfg_var('percent_cols')),
 		asset_limit     = get_cfg_var('asset_limit'),
 		cached_data     = get_cfg_var('cached_data'),
@@ -835,6 +835,8 @@ def make_cfg(gcfg_arg):
 		quiet           = get_cfg_var('quiet'),
 		verbose         = get_cfg_var('verbose'))
 
+	return (src_cls, cfg_in)
+
 def get_cfg_in():
 	ret = namedtuple('cfg_in_data', ['cfg', 'portfolio', 'cfg_file', 'portfolio_file'])
 	cfg_file, portfolio_file = (
@@ -871,17 +873,15 @@ class Ticker:
 
 			self.col1_wid = max(len('TOTAL'), (
 				max(len(self.create_label(d['id'])) for d in data.values()) if cfg.name_labels else
-				max(len(d['symbol']) for d in data.values()))) + 1
+				max(len(d['symbol']) for d in data.values())))
 
-			self.rows = [row._replace(id=self.get_id(row)) if isinstance(row, tuple) else row
-				for row in cfg.rows]
+			self.rows = RowDict(
+				{k: tuple(row._replace(id=self.get_id(row)) for row in v) for k, v in cfg.rows.items()})
 			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 isinstance(row, tuple) and row.id in data}
+			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')
 
-		def format_last_update_col(self, cross_assets=()):
+		def format_last_updated_col(self, cross_assets=()):
 
 			if cfg.elapsed:
 				from mmgen.util2 import format_elapsed_hr
@@ -899,19 +899,18 @@ class Ticker:
 				min_t = None
 
 			for row in self.rows:
-				if isinstance(row, tuple):
-					try:
-						t = int(d[row.id]['last_updated'])
-					except TypeError as e:
-						d[row.id]['last_updated_fmt'] = gray('--' if 'NoneType' in str(e) else str(e))
-					except KeyError as e:
-						msg(str(e))
-						pass
-					else:
-						t_fmt = d[row.id]['last_updated_fmt'] = fmt_func(
-							(min(t, min_t) if min_t else t),
-							now = now)
-						max_w = max(len(t_fmt), max_w)
+				try:
+					t = int(d[row.id]['last_updated'])
+				except TypeError as e:
+					d[row.id]['last_updated_fmt'] = gray('--' if 'NoneType' in str(e) else str(e))
+				except KeyError as e:
+					msg(str(e))
+					pass
+				else:
+					t_fmt = d[row.id]['last_updated_fmt'] = fmt_func(
+						(min(t, min_t) if min_t else t),
+						now = now)
+					max_w = max(len(t_fmt), max_w)
 
 			self.upd_w = max_w
 
@@ -924,8 +923,9 @@ class Ticker:
 			if asset.id:
 				return asset.id
 			else:
+				m = asset.symbol
 				for d in self.data.values():
-					if d['symbol'] == asset.symbol:
+					if m == d['symbol']:
 						return d['id']
 
 		def create_label(self, id):
@@ -961,16 +961,22 @@ class Ticker:
 			if self.table_hdr:
 				yield self.table_hdr
 
-			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:
+			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})')
+			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 '-' * self.hl_wid
 
@@ -1003,12 +1009,12 @@ class Ticker:
 
 			super().__init__(data)
 
-			self.format_last_update_col()
+			self.format_last_updated_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)
+						if row.id in cfg.portfolio and row.id in data)
 							for col_id in self.col_ids}
 
 			self.init_prec()
@@ -1036,6 +1042,7 @@ class Ticker:
 
 			return self.fs_num.format(
 				idx = idx,
+				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')),
 				pc2 = fmt_pct(d.get('percent_change_24h')),
@@ -1072,9 +1079,9 @@ class Ticker:
 			) 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 (
+				['label', 'usd-us-dollar']
+				+ [asset.id for asset in self.usr_col_assets]
+				+ [a for a, b in (
 					('btc-bitcoin',  not cfg.btc_only),
 					('pct1y',       'y' in cfg.percent_cols),
 					('pct1m',       'm' in cfg.percent_cols),
@@ -1082,6 +1089,14 @@ class Ticker:
 					('pct1d',       'd' in cfg.percent_cols),
 					('update_time', cfg.update_time))
 						if b])
+
+			if cfg.asset_range:
+				num_w = len(str(len(cfg.rows['asset_list'])))
+				col_fs_data.update({
+					'idx': fd(' ' * (num_w + 2), f'{{idx:{num_w}}}) ', num_w + 2),
+					'mcap': fd('{mcap:>12}', '{mcap:12.5f}', 12)})
+				cols = ['idx', 'label', 'mcap'] + cols[1:]
+
 			cols2 = list(cols)
 			if cfg.update_time:
 				cols2.pop()
@@ -1095,19 +1110,11 @@ 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(
 				lbl = '',
+				mcap = 'MarketCap(B)',
 				pc1 = ' CHG_7d',
 				pc2 = 'CHG_24h',
 				pc3 = 'CHG_1y',
@@ -1152,7 +1159,7 @@ class Ticker:
 			for a in self.usr_col_assets:
 				self.prices[a.id]['usd-us-dollar'] = data[a.id]['price_usd']
 
-			self.format_last_update_col(cross_assets=self.usr_col_assets)
+			self.format_last_updated_col(cross_assets=self.usr_col_assets)
 
 			self.init_prec()
 			self.init_fs()

+ 1 - 1
mmgen_node_tools/data/version

@@ -1 +1 @@
-3.6.dev5
+3.6.dev6

+ 2 - 2
mmgen_node_tools/main_ticker.py

@@ -244,9 +244,9 @@ from . import Ticker
 
 gcfg = Config(opts_data=opts_data, caller_post_init=True)
 
-Ticker.make_cfg(gcfg)
+src_cls, cfg_in = Ticker.make_cfg(gcfg)
 
-from .Ticker import dfl_cachedir, homedir, DataSource, assets_list_gen, cfg_in, src_cls
+from .Ticker import dfl_cachedir, homedir, DataSource, assets_list_gen
 
 gcfg._post_init()
 

+ 14 - 10
test/cmdtest_d/misc.py

@@ -328,8 +328,8 @@ class CmdTestScripts(CmdTestBase):
 		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'
+				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'
 			],
 			add_opts = ['--widest', '--add-columns=eurusd=x'])
 
@@ -337,9 +337,11 @@ class CmdTestScripts(CmdTestBase):
 		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',
+				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'
+				' ---------------------------------------------------------'
 			],
 			add_opts = ['--add-columns=eurusd=x'])
 
@@ -347,9 +349,10 @@ class CmdTestScripts(CmdTestBase):
 		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',
+				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',
 			],
 			add_opts = ['--add-columns=eurusd=x'])
 
@@ -357,7 +360,8 @@ class CmdTestScripts(CmdTestBase):
 		return self.ticker(
 			['5-5'],
 			[
-				'USD EURUSD=X BTC '
-				r'5\) ADA 0.51 0.4764 0.00002180',
+				r'MarketCap\(B\) USD EURUSD=X BTC '
+				'--------------------------------------------------------- '
+				r'5\) ADA 17.11161 0.51 0.4764 0.00002180',
 			],
 			add_opts = ['--add-columns=eurusd=x'])