From 060b968ad4b88f845b9cd95fa89174a69b4b9743 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Mon, 13 Oct 2025 14:59:00 +0000 Subject: [PATCH] mmnode-ticker: display crypto assets by market cap Examples: # Display top 2000 assets by market cap: $ mmnode-ticker 2000 # Display assets 201-300 by market cap, displaying all available columns: $ mmnode-ticker --widest 201-300 # Display asset 32 by market cap: $ mmnode-ticker 32-32 Testing/demo: $ test/cmdtest.py -e scripts.ticker --- mmgen_node_tools/Ticker.py | 53 +++++++++++++++++++++++++++++---- mmgen_node_tools/data/version | 2 +- mmgen_node_tools/main_ticker.py | 25 +++++++++++----- test/cmdtest_d/misc.py | 42 ++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 13 deletions(-) diff --git a/mmgen_node_tools/Ticker.py b/mmgen_node_tools/Ticker.py index 9d1c9df..bf3398b 100755 --- a/mmgen_node_tools/Ticker.py +++ b/mmgen_node_tools/Ticker.py @@ -182,6 +182,7 @@ 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) if is_int(cfg.asset_limit) else self.dfl_asset_limit @@ -576,6 +577,17 @@ def main(): 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 @@ -643,6 +655,21 @@ def make_cfg(gcfg_arg): 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]] @@ -731,6 +758,7 @@ def make_cfg(gcfg_arg): 'usr_rows', 'usr_columns', 'query', + 'asset_range', 'adjust', 'clsname', 'btc_only', @@ -767,8 +795,10 @@ def make_cfg(gcfg_arg): if cmd_args := gcfg._args: if len(cmd_args) > 1: die(1, 'Only one command-line argument is allowed') - query = parse_query_arg(cmd_args[0]) + 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') @@ -783,6 +813,7 @@ 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 = get_cfg_var('btc'), @@ -930,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})') @@ -989,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}') @@ -1002,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')), @@ -1061,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( @@ -1136,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) diff --git a/mmgen_node_tools/data/version b/mmgen_node_tools/data/version index 4e5cebc..636c831 100644 --- a/mmgen_node_tools/data/version +++ b/mmgen_node_tools/data/version @@ -1 +1 @@ -3.6.dev4 +3.6.dev5 diff --git a/mmgen_node_tools/main_ticker.py b/mmgen_node_tools/main_ticker.py index c36d886..8efb97a 100755 --- a/mmgen_node_tools/main_ticker.py +++ b/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} diff --git a/test/cmdtest_d/misc.py b/test/cmdtest_d/misc.py index 1cde41e..c2dceff 100755 --- a/test/cmdtest_d/misc.py +++ b/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'])