meshtensor-cli 9.18.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. meshtensor_cli/__init__.py +22 -0
  2. meshtensor_cli/cli.py +10742 -0
  3. meshtensor_cli/doc_generation_helper.py +4 -0
  4. meshtensor_cli/src/__init__.py +1085 -0
  5. meshtensor_cli/src/commands/__init__.py +0 -0
  6. meshtensor_cli/src/commands/axon/__init__.py +0 -0
  7. meshtensor_cli/src/commands/axon/axon.py +132 -0
  8. meshtensor_cli/src/commands/crowd/__init__.py +0 -0
  9. meshtensor_cli/src/commands/crowd/contribute.py +621 -0
  10. meshtensor_cli/src/commands/crowd/contributors.py +200 -0
  11. meshtensor_cli/src/commands/crowd/create.py +783 -0
  12. meshtensor_cli/src/commands/crowd/dissolve.py +219 -0
  13. meshtensor_cli/src/commands/crowd/refund.py +233 -0
  14. meshtensor_cli/src/commands/crowd/update.py +418 -0
  15. meshtensor_cli/src/commands/crowd/utils.py +124 -0
  16. meshtensor_cli/src/commands/crowd/view.py +991 -0
  17. meshtensor_cli/src/commands/governance/__init__.py +0 -0
  18. meshtensor_cli/src/commands/governance/governance.py +794 -0
  19. meshtensor_cli/src/commands/liquidity/__init__.py +0 -0
  20. meshtensor_cli/src/commands/liquidity/liquidity.py +699 -0
  21. meshtensor_cli/src/commands/liquidity/utils.py +202 -0
  22. meshtensor_cli/src/commands/proxy.py +700 -0
  23. meshtensor_cli/src/commands/stake/__init__.py +0 -0
  24. meshtensor_cli/src/commands/stake/add.py +799 -0
  25. meshtensor_cli/src/commands/stake/auto_staking.py +306 -0
  26. meshtensor_cli/src/commands/stake/children_hotkeys.py +865 -0
  27. meshtensor_cli/src/commands/stake/claim.py +770 -0
  28. meshtensor_cli/src/commands/stake/list.py +738 -0
  29. meshtensor_cli/src/commands/stake/move.py +1211 -0
  30. meshtensor_cli/src/commands/stake/remove.py +1466 -0
  31. meshtensor_cli/src/commands/stake/wizard.py +323 -0
  32. meshtensor_cli/src/commands/subnets/__init__.py +0 -0
  33. meshtensor_cli/src/commands/subnets/mechanisms.py +515 -0
  34. meshtensor_cli/src/commands/subnets/price.py +733 -0
  35. meshtensor_cli/src/commands/subnets/subnets.py +2908 -0
  36. meshtensor_cli/src/commands/sudo.py +1294 -0
  37. meshtensor_cli/src/commands/tc/__init__.py +0 -0
  38. meshtensor_cli/src/commands/tc/tc.py +190 -0
  39. meshtensor_cli/src/commands/treasury/__init__.py +0 -0
  40. meshtensor_cli/src/commands/treasury/treasury.py +194 -0
  41. meshtensor_cli/src/commands/view.py +354 -0
  42. meshtensor_cli/src/commands/wallets.py +2311 -0
  43. meshtensor_cli/src/commands/weights.py +467 -0
  44. meshtensor_cli/src/meshtensor/__init__.py +0 -0
  45. meshtensor_cli/src/meshtensor/balances.py +313 -0
  46. meshtensor_cli/src/meshtensor/chain_data.py +1263 -0
  47. meshtensor_cli/src/meshtensor/extrinsics/__init__.py +0 -0
  48. meshtensor_cli/src/meshtensor/extrinsics/mev_shield.py +174 -0
  49. meshtensor_cli/src/meshtensor/extrinsics/registration.py +1861 -0
  50. meshtensor_cli/src/meshtensor/extrinsics/root.py +550 -0
  51. meshtensor_cli/src/meshtensor/extrinsics/serving.py +255 -0
  52. meshtensor_cli/src/meshtensor/extrinsics/transfer.py +239 -0
  53. meshtensor_cli/src/meshtensor/meshtensor_interface.py +2598 -0
  54. meshtensor_cli/src/meshtensor/minigraph.py +254 -0
  55. meshtensor_cli/src/meshtensor/networking.py +12 -0
  56. meshtensor_cli/src/meshtensor/templates/main-filters.j2 +24 -0
  57. meshtensor_cli/src/meshtensor/templates/main-header.j2 +36 -0
  58. meshtensor_cli/src/meshtensor/templates/neuron-details.j2 +111 -0
  59. meshtensor_cli/src/meshtensor/templates/price-multi.j2 +113 -0
  60. meshtensor_cli/src/meshtensor/templates/price-single.j2 +99 -0
  61. meshtensor_cli/src/meshtensor/templates/subnet-details-header.j2 +49 -0
  62. meshtensor_cli/src/meshtensor/templates/subnet-details.j2 +32 -0
  63. meshtensor_cli/src/meshtensor/templates/subnet-metrics.j2 +57 -0
  64. meshtensor_cli/src/meshtensor/templates/subnets-table.j2 +28 -0
  65. meshtensor_cli/src/meshtensor/templates/table.j2 +267 -0
  66. meshtensor_cli/src/meshtensor/templates/view.css +1058 -0
  67. meshtensor_cli/src/meshtensor/templates/view.j2 +43 -0
  68. meshtensor_cli/src/meshtensor/templates/view.js +1053 -0
  69. meshtensor_cli/src/meshtensor/utils.py +2007 -0
  70. meshtensor_cli/version.py +23 -0
  71. meshtensor_cli-9.18.1.dist-info/METADATA +261 -0
  72. meshtensor_cli-9.18.1.dist-info/RECORD +74 -0
  73. meshtensor_cli-9.18.1.dist-info/WHEEL +4 -0
  74. meshtensor_cli-9.18.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,733 @@
1
+ import asyncio
2
+ import json
3
+ import math
4
+ import tempfile
5
+ import webbrowser
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ import plotille
10
+ import plotly.graph_objects as go
11
+
12
+ from meshtensor_cli.src import COLOR_PALETTE
13
+ from meshtensor_cli.src.meshtensor.chain_data import DynamicInfo
14
+ from meshtensor_cli.src.meshtensor.utils import (
15
+ console,
16
+ get_subnet_name,
17
+ print_error,
18
+ json_console,
19
+ jinja_env,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from meshtensor_cli.src.meshtensor.meshtensor_interface import MeshtensorInterface
24
+
25
+
26
+ async def price(
27
+ meshtensor: "MeshtensorInterface",
28
+ netuids: list[int],
29
+ all_netuids: bool = False,
30
+ interval_hours: int = 4,
31
+ current_only: bool = False,
32
+ html_output: bool = False,
33
+ log_scale: bool = False,
34
+ json_output: bool = False,
35
+ ):
36
+ """
37
+ Fetch historical price data for subnets and display it in a chart.
38
+ """
39
+ if all_netuids:
40
+ netuids = [nid for nid in await meshtensor.get_all_subnet_netuids() if nid != 0]
41
+
42
+ blocks_per_hour = int(3600 / 12) # ~300 blocks per hour
43
+ total_blocks = blocks_per_hour * interval_hours
44
+
45
+ if not current_only:
46
+ with console.status(":chart_increasing: Fetching historical price data..."):
47
+ current_block_hash = await meshtensor.substrate.get_chain_head()
48
+ current_block = await meshtensor.substrate.get_block_number(
49
+ current_block_hash
50
+ )
51
+
52
+ step = 300
53
+ start_block = max(0, current_block - total_blocks)
54
+
55
+ # snap start block down to nearest multiple of 10
56
+ start_block -= start_block % 10
57
+
58
+ block_numbers = []
59
+ for b in range(start_block, current_block + 1, step):
60
+ if b == current_block:
61
+ block_numbers.append(b) # exact current block
62
+ else:
63
+ block_numbers.append(b - (b % 5)) # snap down to multiple of 10
64
+ block_numbers = sorted(set(block_numbers))
65
+
66
+ # Block hashes
67
+ block_hash_cors = [
68
+ meshtensor.substrate.get_block_hash(bn) for bn in block_numbers
69
+ ]
70
+ block_hashes = await asyncio.gather(*block_hash_cors)
71
+
72
+ # We fetch all subnets when there is more than one netuid
73
+ if all_netuids or len(netuids) > 1:
74
+ subnet_info_cors = [meshtensor.all_subnets(bh) for bh in block_hashes]
75
+ else:
76
+ # If there is only one netuid, we fetch the subnet info for that netuid
77
+ netuid = netuids[0]
78
+ subnet_info_cors = [meshtensor.subnet(netuid, bh) for bh in block_hashes]
79
+ all_subnet_infos = await asyncio.gather(*subnet_info_cors)
80
+
81
+ subnet_data = _process_subnet_data(
82
+ block_numbers, all_subnet_infos, netuids, all_netuids
83
+ )
84
+ if not subnet_data:
85
+ print_error("No valid price data found for any subnet")
86
+ return
87
+
88
+ if html_output:
89
+ await _generate_html_output(
90
+ subnet_data, block_numbers, interval_hours, log_scale
91
+ )
92
+ elif json_output:
93
+ json_console.print(json.dumps(_generate_json_output(subnet_data)))
94
+ else:
95
+ _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale)
96
+ else:
97
+ with console.status("Fetching current price data..."):
98
+ if all_netuids or len(netuids) > 1:
99
+ all_subnet_info = await meshtensor.all_subnets()
100
+ else:
101
+ all_subnet_info = [await meshtensor.subnet(netuid=netuids[0])]
102
+ subnet_data = _process_current_subnet_data(
103
+ all_subnet_info, netuids, all_netuids
104
+ )
105
+ if json_output:
106
+ json_console.print(json.dumps(_generate_json_output(subnet_data)))
107
+ else:
108
+ _generate_cli_output_current(subnet_data)
109
+
110
+
111
+ def _process_current_subnet_data(subnet_infos: list[DynamicInfo], netuids, all_netuids):
112
+ subnet_data = {}
113
+ if all_netuids or len(netuids) > 1:
114
+ # Most recent data for statistics
115
+ for subnet_info in subnet_infos:
116
+ stats = {
117
+ "current_price": subnet_info.price,
118
+ "supply": subnet_info.alpha_in.tao + subnet_info.alpha_out.tao,
119
+ "market_cap": subnet_info.price.tao
120
+ * (subnet_info.alpha_in.tao + subnet_info.alpha_out.tao),
121
+ "emission": subnet_info.emission.tao,
122
+ "stake": subnet_info.alpha_out.tao,
123
+ "symbol": subnet_info.symbol,
124
+ "name": get_subnet_name(subnet_info),
125
+ }
126
+ subnet_data[subnet_info.netuid] = {
127
+ "stats": stats,
128
+ }
129
+ else:
130
+ subnet_info = subnet_infos[0]
131
+ stats = {
132
+ "current_price": subnet_info.price.tao,
133
+ "supply": subnet_info.alpha_in.tao + subnet_info.alpha_out.tao,
134
+ "market_cap": subnet_info.price.tao
135
+ * (subnet_info.alpha_in.tao + subnet_info.alpha_out.tao),
136
+ "emission": subnet_info.emission.tao,
137
+ "stake": subnet_info.alpha_out.tao,
138
+ "symbol": subnet_info.symbol,
139
+ "name": get_subnet_name(subnet_info),
140
+ }
141
+ subnet_data[subnet_info.netuid] = {
142
+ "stats": stats,
143
+ }
144
+ return subnet_data
145
+
146
+
147
+ def _process_subnet_data(block_numbers, all_subnet_infos, netuids, all_netuids):
148
+ """
149
+ Process subnet data into a structured format for price analysis.
150
+ """
151
+ subnet_data = {}
152
+ if all_netuids or len(netuids) > 1:
153
+ for netuid in netuids:
154
+ prices = []
155
+ valid_subnet_infos = []
156
+ for _, subnet_infos in zip(block_numbers, all_subnet_infos):
157
+ subnet_info = next(
158
+ (s for s in subnet_infos if s.netuid == netuid), None
159
+ )
160
+ if subnet_info:
161
+ prices.append(subnet_info.price.tao)
162
+ valid_subnet_infos.append(subnet_info)
163
+
164
+ if not valid_subnet_infos or not prices:
165
+ # No valid data found for this netuid
166
+ continue
167
+
168
+ if len(prices) < 5:
169
+ print_error(
170
+ f"Insufficient price data for subnet {netuid}. "
171
+ f"Need at least 5 data points but only found {len(prices)}."
172
+ )
173
+ continue
174
+
175
+ # Most recent data for statistics
176
+ latest_subnet_data = valid_subnet_infos[-1]
177
+ stats = {
178
+ "current_price": prices[-1],
179
+ "high": max(prices),
180
+ "low": min(prices),
181
+ "change_pct": ((prices[-1] - prices[0]) / prices[0] * 100),
182
+ "supply": latest_subnet_data.alpha_in.tao
183
+ + latest_subnet_data.alpha_out.tao,
184
+ "market_cap": latest_subnet_data.price.tao
185
+ * (latest_subnet_data.alpha_in.tao + latest_subnet_data.alpha_out.tao),
186
+ "emission": latest_subnet_data.emission.tao,
187
+ "stake": latest_subnet_data.alpha_out.tao,
188
+ "symbol": latest_subnet_data.symbol,
189
+ "name": get_subnet_name(latest_subnet_data),
190
+ }
191
+ subnet_data[netuid] = {
192
+ "prices": prices,
193
+ "stats": stats,
194
+ }
195
+
196
+ else:
197
+ prices = []
198
+ valid_subnet_infos = []
199
+ for _, subnet_info in zip(block_numbers, all_subnet_infos):
200
+ if subnet_info:
201
+ prices.append(subnet_info.price.tao)
202
+ valid_subnet_infos.append(subnet_info)
203
+
204
+ if not valid_subnet_infos or not prices:
205
+ print_error("No valid price data found for any subnet")
206
+ return {}
207
+
208
+ if len(prices) < 5:
209
+ print_error(
210
+ f"Insufficient price data for subnet {netuids[0]}. "
211
+ f"Need at least 5 data points but only found {len(prices)}."
212
+ )
213
+ return {}
214
+
215
+ # Most recent data for statistics
216
+ latest_subnet_data = valid_subnet_infos[-1]
217
+ stats = {
218
+ "current_price": prices[-1],
219
+ "high": max(prices),
220
+ "low": min(prices),
221
+ "change_pct": ((prices[-1] - prices[0]) / prices[0] * 100),
222
+ "supply": latest_subnet_data.alpha_in.tao
223
+ + latest_subnet_data.alpha_out.tao,
224
+ "market_cap": latest_subnet_data.price.tao
225
+ * (latest_subnet_data.alpha_in.tao + latest_subnet_data.alpha_out.tao),
226
+ "emission": latest_subnet_data.emission.tao,
227
+ "stake": latest_subnet_data.alpha_out.tao,
228
+ "symbol": latest_subnet_data.symbol,
229
+ "name": get_subnet_name(latest_subnet_data),
230
+ }
231
+ subnet_data[netuids[0]] = {
232
+ "prices": prices,
233
+ "stats": stats,
234
+ }
235
+
236
+ # Sort results by market cap
237
+ sorted_subnet_data = dict(
238
+ sorted(
239
+ subnet_data.items(),
240
+ key=lambda x: x[1]["stats"]["market_cap"],
241
+ reverse=True,
242
+ )
243
+ )
244
+ return sorted_subnet_data
245
+
246
+
247
+ def _generate_html_single_subnet(
248
+ netuid, data, block_numbers, interval_hours, log_scale, title: str
249
+ ):
250
+ """
251
+ Generate an HTML chart for a single subnet.
252
+ """
253
+ stats = data["stats"]
254
+ prices = data["prices"]
255
+
256
+ fig = go.Figure()
257
+ fig.add_trace(
258
+ go.Scatter(
259
+ x=block_numbers,
260
+ y=prices,
261
+ mode="lines",
262
+ name=f"Subnet {netuid} - {stats['name']}"
263
+ if stats["name"]
264
+ else f"Subnet {netuid}",
265
+ line=dict(width=2, color="#50C878"),
266
+ )
267
+ )
268
+
269
+ fig.update_layout(
270
+ template="plotly_dark",
271
+ paper_bgcolor="#000000",
272
+ plot_bgcolor="#000000",
273
+ font=dict(color="white"),
274
+ showlegend=True,
275
+ legend=dict(
276
+ x=1.02,
277
+ y=1.0,
278
+ xanchor="left",
279
+ yanchor="top",
280
+ bgcolor="rgba(0,0,0,0)",
281
+ bordercolor="rgba(255,255,255,0.2)",
282
+ borderwidth=1,
283
+ ),
284
+ margin=dict(t=160, r=50, b=50, l=50),
285
+ height=600,
286
+ )
287
+
288
+ price_title = f"Price ({stats['symbol']})"
289
+ if log_scale:
290
+ price_title += " Log Scale"
291
+
292
+ # Label axes
293
+ fig.update_xaxes(
294
+ title="Block",
295
+ gridcolor="rgba(128,128,128,0.2)",
296
+ zerolinecolor="rgba(128,128,128,0.2)",
297
+ type="log" if log_scale else "linear",
298
+ )
299
+ fig.update_yaxes(
300
+ title=price_title,
301
+ gridcolor="rgba(128,128,128,0.2)",
302
+ zerolinecolor="rgba(128,128,128,0.2)",
303
+ type="log" if log_scale else "linear",
304
+ )
305
+
306
+ fig_json = fig.to_json()
307
+
308
+ template = jinja_env.get_template("price-single.j2")
309
+ html_content = template.render(
310
+ fig_json=fig_json,
311
+ stats=stats,
312
+ change_pct=abs(stats["change_pct"]),
313
+ interval_hours=interval_hours,
314
+ title=title,
315
+ )
316
+ return html_content
317
+
318
+
319
+ def _generate_html_multi_subnet(
320
+ subnet_data, block_numbers, interval_hours, log_scale, title: str
321
+ ):
322
+ """
323
+ Generate an HTML chart for multiple subnets.
324
+ """
325
+ # Pick top subnet by market cap
326
+ top_subnet_netuid = max(
327
+ subnet_data.keys(),
328
+ key=lambda k: subnet_data[k]["stats"]["market_cap"],
329
+ )
330
+ top_subnet_stats = subnet_data[top_subnet_netuid]["stats"]
331
+
332
+ fig = go.Figure()
333
+ fig.update_layout(
334
+ template="plotly_dark",
335
+ paper_bgcolor="#000000",
336
+ plot_bgcolor="#000000",
337
+ font=dict(color="white"),
338
+ showlegend=True,
339
+ legend=dict(
340
+ x=1.02,
341
+ y=1.0,
342
+ xanchor="left",
343
+ yanchor="top",
344
+ bgcolor="rgba(0,0,0,0)",
345
+ bordercolor="rgba(255,255,255,0.2)",
346
+ borderwidth=1,
347
+ ),
348
+ margin=dict(t=200, r=80, b=50, l=50),
349
+ height=700,
350
+ )
351
+
352
+ price_title = "Price (τ)"
353
+ if log_scale:
354
+ price_title += " Log Scale"
355
+
356
+ # Label axes
357
+ fig.update_xaxes(
358
+ title="Block",
359
+ gridcolor="rgba(128,128,128,0.2)",
360
+ zerolinecolor="rgba(128,128,128,0.2)",
361
+ type="log" if log_scale else "linear",
362
+ )
363
+ fig.update_yaxes(
364
+ title=price_title,
365
+ gridcolor="rgba(128,128,128,0.2)",
366
+ zerolinecolor="rgba(128,128,128,0.2)",
367
+ type="log" if log_scale else "linear",
368
+ )
369
+
370
+ # Create annotation for top subnet
371
+ sign_icon = "▲" if top_subnet_stats["change_pct"] > 0 else "▼"
372
+ change_color = "#00FF00" if top_subnet_stats["change_pct"] > 0 else "#FF5555"
373
+
374
+ left_text = (
375
+ f"Top subnet: Subnet {top_subnet_netuid}"
376
+ + (f" - {top_subnet_stats['name']}" if top_subnet_stats["name"] else "")
377
+ + "<br><br>"
378
+ + f"<span style='font-size: 24px'>{top_subnet_stats['current_price']:.6f} {top_subnet_stats['symbol']}"
379
+ + f"<span style='color: {change_color}'> {sign_icon} {abs(top_subnet_stats['change_pct']):.2f}%</span></span><br><br>"
380
+ + f"{interval_hours}h High: <span style='color: #00FF00'>{top_subnet_stats['high']:.6f}</span>, "
381
+ + f"Low: <span style='color: #FF5555'>{top_subnet_stats['low']:.6f}</span>"
382
+ )
383
+
384
+ right_text = (
385
+ f"Supply: <span style='color: #87CEEB'>{top_subnet_stats['supply']:.2f} {top_subnet_stats['symbol']}</span><br>"
386
+ f"Market Cap: <span style='color: #4682B4'>{top_subnet_stats['market_cap']:.2f} τ</span><br>"
387
+ f"Emission: <span style='color: #DDA0DD'>{top_subnet_stats['emission']:.2f} {top_subnet_stats['symbol']}</span><br>"
388
+ f"Stake: <span style='color: #FFD700'>{top_subnet_stats['stake']:.2f} {top_subnet_stats['symbol']}</span>"
389
+ )
390
+
391
+ all_annotations = [
392
+ dict(
393
+ text=left_text,
394
+ x=0.0,
395
+ y=1.3,
396
+ xref="paper",
397
+ yref="paper",
398
+ align="left",
399
+ showarrow=False,
400
+ font=dict(size=14),
401
+ xanchor="left",
402
+ yanchor="top",
403
+ ),
404
+ dict(
405
+ text=right_text,
406
+ x=1.02,
407
+ y=1.3,
408
+ xref="paper",
409
+ yref="paper",
410
+ align="left",
411
+ showarrow=False,
412
+ font=dict(size=14),
413
+ xanchor="left",
414
+ yanchor="top",
415
+ ),
416
+ ]
417
+
418
+ fig.update_layout(annotations=all_annotations)
419
+
420
+ # Generate colors for subnets
421
+ def generate_color_palette(n):
422
+ """Generate n distinct colors using a variation of HSV color space."""
423
+ colors = []
424
+ for i in range(n):
425
+ hue = i * 0.618033988749895 % 1
426
+ saturation = 0.6 + (i % 3) * 0.2
427
+ value = 0.8 + (i % 2) * 0.2 # Brightness
428
+
429
+ h = hue * 6
430
+ c = value * saturation
431
+ x = c * (1 - abs(h % 2 - 1))
432
+ m = value - c
433
+
434
+ if h < 1:
435
+ r, g, b = c, x, 0
436
+ elif h < 2:
437
+ r, g, b = x, c, 0
438
+ elif h < 3:
439
+ r, g, b = 0, c, x
440
+ elif h < 4:
441
+ r, g, b = 0, x, c
442
+ elif h < 5:
443
+ r, g, b = x, 0, c
444
+ else:
445
+ r, g, b = c, 0, x
446
+
447
+ rgb = (
448
+ int((r + m) * 255),
449
+ int((g + m) * 255),
450
+ int((b + m) * 255),
451
+ )
452
+ colors.append(f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}")
453
+ return colors
454
+
455
+ base_colors = generate_color_palette(len(subnet_data) + 1)
456
+
457
+ # Plot each subnet as a separate trace
458
+ subnet_keys = list(subnet_data.keys())
459
+ for i, netuid in enumerate(subnet_keys):
460
+ d = subnet_data[netuid]
461
+ fig.add_trace(
462
+ go.Scatter(
463
+ x=block_numbers,
464
+ y=d["prices"],
465
+ mode="lines",
466
+ name=(
467
+ f"Subnet {netuid} - {d['stats']['name']}"
468
+ if d["stats"]["name"]
469
+ else f"Subnet {netuid}"
470
+ ),
471
+ line=dict(width=2, color=base_colors[i]),
472
+ visible=True,
473
+ )
474
+ )
475
+
476
+ # Annotations for each subnet
477
+ def build_single_subnet_annotations(netuid):
478
+ s = subnet_data[netuid]["stats"]
479
+ name_line = f"Subnet {netuid}" + (f" - {s['name']}" if s["name"] else "")
480
+
481
+ sign_icon = "▲" if s["change_pct"] > 0 else "▼"
482
+ change_color = "#00FF00" if s["change_pct"] > 0 else "#FF5555"
483
+
484
+ left_text = (
485
+ f"{name_line}<br><br>"
486
+ f"<span style='font-size: 24px'>{s['current_price']:.6f} {s['symbol']}"
487
+ f"<span style='color: {change_color}'> {sign_icon} {abs(s['change_pct']):.2f}%</span></span><br><br>"
488
+ f"{interval_hours}h High: <span style='color: #00FF00'>{s['high']:.6f}</span>, "
489
+ f"Low: <span style='color: #FF5555'>{s['low']:.6f}</span>"
490
+ )
491
+
492
+ right_text = (
493
+ f"Supply: <span style='color: #87CEEB'>{s['supply']:.2f} {s['symbol']}</span><br>"
494
+ f"Market Cap: <span style='color: #4682B4'>{s['market_cap']:.2f} τ</span><br>"
495
+ f"Emission: <span style='color: #DDA0DD'>{s['emission']:.2f} {s['symbol']}</span><br>"
496
+ f"Stake: <span style='color: #FFD700'>{s['stake']:.2f} {s['symbol']}</span>"
497
+ )
498
+
499
+ left_annot = dict(
500
+ text=left_text,
501
+ x=0.0,
502
+ y=1.3,
503
+ xref="paper",
504
+ yref="paper",
505
+ align="left",
506
+ showarrow=False,
507
+ font=dict(size=14),
508
+ xanchor="left",
509
+ yanchor="top",
510
+ )
511
+ right_annot = dict(
512
+ text=right_text,
513
+ x=1.02,
514
+ y=1.3,
515
+ xref="paper",
516
+ yref="paper",
517
+ align="left",
518
+ showarrow=False,
519
+ font=dict(size=14),
520
+ xanchor="left",
521
+ yanchor="top",
522
+ )
523
+ return [left_annot, right_annot]
524
+
525
+ # "All" visibility mask
526
+ all_visibility = [True] * len(subnet_keys)
527
+
528
+ # Build visibility masks for each subnet
529
+ subnet_modes = {}
530
+ for idx, netuid in enumerate(subnet_keys):
531
+ single_vis = [False] * len(subnet_keys)
532
+ single_vis[idx] = True
533
+ single_annots = build_single_subnet_annotations(netuid)
534
+ subnet_modes[netuid] = {
535
+ "visible": single_vis,
536
+ "annotations": single_annots,
537
+ }
538
+
539
+ fig_json = fig.to_json()
540
+
541
+ template = jinja_env.get_template("price-multi.j2")
542
+ html_content = template.render(
543
+ title=title,
544
+ # We sort netuids by market cap but for buttons, they are ordered by netuid
545
+ sorted_subnet_keys=sorted(subnet_data.keys()),
546
+ fig_json=fig_json,
547
+ all_visibility=all_visibility,
548
+ all_annotations=all_annotations,
549
+ subnet_modes=subnet_modes,
550
+ )
551
+ return html_content
552
+
553
+
554
+ async def _generate_html_output(
555
+ subnet_data,
556
+ block_numbers,
557
+ interval_hours,
558
+ log_scale: bool = False,
559
+ ):
560
+ """
561
+ Display HTML output in browser
562
+ """
563
+ try:
564
+ subnet_keys = list(subnet_data.keys())
565
+
566
+ # Single subnet
567
+ if len(subnet_keys) == 1:
568
+ netuid = subnet_keys[0]
569
+ data = subnet_data[netuid]
570
+ html_content = _generate_html_single_subnet(
571
+ netuid,
572
+ data,
573
+ block_numbers,
574
+ interval_hours,
575
+ log_scale,
576
+ title=f"Subnet {netuid} Price View",
577
+ )
578
+
579
+ else:
580
+ # Multi-subnet
581
+ html_content = _generate_html_multi_subnet(
582
+ subnet_data,
583
+ block_numbers,
584
+ interval_hours,
585
+ log_scale,
586
+ title="Subnets Price Chart",
587
+ )
588
+
589
+ console.print(
590
+ "[dark_sea_green3]Opening price chart in a window.[/dark_sea_green3]"
591
+ )
592
+ with tempfile.NamedTemporaryFile(
593
+ "w", delete=False, suffix=".html"
594
+ ) as dashboard_file:
595
+ url = f"file://{dashboard_file.name}"
596
+ dashboard_file.write(html_content)
597
+
598
+ webbrowser.open(url, new=1)
599
+ except Exception as e:
600
+ print_error(f"Error generating price chart: {e}")
601
+
602
+
603
+ def _generate_json_output(subnet_data):
604
+ return {netuid: data for netuid, data in subnet_data.items()}
605
+
606
+
607
+ def _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale):
608
+ """
609
+ Render the price data in a textual CLI style with plotille ASCII charts.
610
+ """
611
+ for netuid, data in subnet_data.items():
612
+ fig = plotille.Figure()
613
+ fig.width = 60
614
+ fig.height = 20
615
+ fig.color_mode = "rgb"
616
+ fig.background = None
617
+
618
+ def color_label(text):
619
+ return plotille.color(text, fg=(186, 233, 143), mode="rgb")
620
+
621
+ fig.x_label = color_label("Block")
622
+ y_label_text = f"Price ({data['stats']['symbol']})"
623
+ fig.y_label = color_label(y_label_text)
624
+
625
+ prices = data["prices"]
626
+ if log_scale:
627
+ prices = [math.log10(p) for p in prices]
628
+
629
+ fig.set_x_limits(min_=min(block_numbers), max_=max(block_numbers))
630
+ fig.set_y_limits(
631
+ min_=data["stats"]["low"] * 0.99,
632
+ max_=data["stats"]["high"] * 1.01,
633
+ )
634
+
635
+ fig.plot(
636
+ block_numbers,
637
+ prices,
638
+ label=f"Subnet {netuid} Price",
639
+ interp="linear",
640
+ lc="bae98f",
641
+ )
642
+
643
+ stats = data["stats"]
644
+ change_color = "dark_sea_green3" if stats["change_pct"] > 0 else "red"
645
+
646
+ if netuid != 0:
647
+ console.print(
648
+ f"\n[{COLOR_PALETTE['GENERAL']['SYMBOL']}]Subnet {netuid} - {stats['symbol']} "
649
+ f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE['GENERAL']['SYMBOL']}]\n"
650
+ f"Current: [blue]{stats['current_price']:.6f}{stats['symbol']}[/blue]\n"
651
+ f"{interval_hours}h High: [dark_sea_green3]{stats['high']:.6f}{stats['symbol']}[/dark_sea_green3]\n"
652
+ f"{interval_hours}h Low: [red]{stats['low']:.6f}{stats['symbol']}[/red]\n"
653
+ f"{interval_hours}h Change: [{change_color}]{stats['change_pct']:.2f}%[/{change_color}]\n"
654
+ )
655
+ else:
656
+ console.print(
657
+ f"\n[{COLOR_PALETTE['GENERAL']['SYMBOL']}]Subnet {netuid} - {stats['symbol']} "
658
+ f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE['GENERAL']['SYMBOL']}]\n"
659
+ f"Current: [blue]{stats['symbol']} {stats['current_price']:.6f}[/blue]\n"
660
+ f"{interval_hours}h High: [dark_sea_green3]{stats['symbol']} {stats['high']:.6f}[/dark_sea_green3]\n"
661
+ f"{interval_hours}h Low: [red]{stats['symbol']} {stats['low']:.6f}[/red]\n"
662
+ f"{interval_hours}h Change: [{change_color}]{stats['change_pct']:.2f}%[/{change_color}]\n"
663
+ )
664
+
665
+ print(fig.show())
666
+
667
+ if netuid != 0:
668
+ stats_text = (
669
+ "\nLatest stats:\n"
670
+ f"Supply: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]"
671
+ f"{stats['supply']:,.2f} {stats['symbol']}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]\n"
672
+ f"Market Cap: [steel_blue3]{stats['market_cap']:,.2f} {stats['symbol']} / 21M[/steel_blue3]\n"
673
+ f"Emission: [{COLOR_PALETTE['POOLS']['EMISSION']}]"
674
+ f"{stats['emission']:,.2f} {stats['symbol']}[/{COLOR_PALETTE['POOLS']['EMISSION']}]\n"
675
+ f"Stake: [{COLOR_PALETTE['STAKE']['MESH']}]"
676
+ f"{stats['stake']:,.2f} {stats['symbol']}[/{COLOR_PALETTE['STAKE']['MESH']}]"
677
+ )
678
+ else:
679
+ stats_text = (
680
+ "\nLatest stats:\n"
681
+ f"Supply: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]"
682
+ f"{stats['symbol']} {stats['supply']:,.2f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]\n"
683
+ f"Market Cap: [steel_blue3]{stats['symbol']} {stats['market_cap']:,.2f} / 21M[/steel_blue3]\n"
684
+ f"Emission: [{COLOR_PALETTE['POOLS']['EMISSION']}]"
685
+ f"{stats['symbol']} {stats['emission']:,.2f}[/{COLOR_PALETTE['POOLS']['EMISSION']}]\n"
686
+ f"Stake: [{COLOR_PALETTE['STAKE']['MESH']}]"
687
+ f"{stats['symbol']} {stats['stake']:,.2f}[/{COLOR_PALETTE['STAKE']['MESH']}]"
688
+ )
689
+
690
+ console.print(stats_text)
691
+
692
+
693
+ def _generate_cli_output_current(subnet_data):
694
+ for netuid, data in subnet_data.items():
695
+ stats = data["stats"]
696
+
697
+ if netuid != 0:
698
+ console.print(
699
+ f"\n[{COLOR_PALETTE.G.SYM}]Subnet {netuid} - {stats['symbol']} "
700
+ f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE.G.SYM}]\n"
701
+ f"Current: [blue]{stats['current_price']:.6f}{stats['symbol']}[/blue]\n"
702
+ )
703
+ else:
704
+ console.print(
705
+ f"\n[{COLOR_PALETTE.G.SYM}]Subnet {netuid} - {stats['symbol']} "
706
+ f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE.G.SYM}]\n"
707
+ f"Current: [blue]{stats['symbol']} {stats['current_price']:.6f}[/blue]\n"
708
+ )
709
+
710
+ if netuid != 0:
711
+ stats_text = (
712
+ "\nLatest stats:\n"
713
+ f"Supply: [{COLOR_PALETTE.P.ALPHA_IN}]"
714
+ f"{stats['supply']:,.2f} {stats['symbol']}[/{COLOR_PALETTE.P.ALPHA_IN}]\n"
715
+ f"Market Cap: [steel_blue3]{stats['market_cap']:,.2f} {stats['symbol']} / 21M[/steel_blue3]\n"
716
+ f"Emission: [{COLOR_PALETTE.P.EMISSION}]"
717
+ f"{stats['emission']:,.2f} {stats['symbol']}[/{COLOR_PALETTE.P.EMISSION}]\n"
718
+ f"Stake: [{COLOR_PALETTE.S.MESH}]"
719
+ f"{stats['stake']:,.2f} {stats['symbol']}[/{COLOR_PALETTE.S.MESH}]"
720
+ )
721
+ else:
722
+ stats_text = (
723
+ "\nLatest stats:\n"
724
+ f"Supply: [{COLOR_PALETTE.P.ALPHA_IN}]"
725
+ f"{stats['symbol']} {stats['supply']:,.2f}[/{COLOR_PALETTE.P.ALPHA_IN}]\n"
726
+ f"Market Cap: [steel_blue3]{stats['symbol']} {stats['market_cap']:,.2f} / 21M[/steel_blue3]\n"
727
+ f"Emission: [{COLOR_PALETTE.P.EMISSION}]"
728
+ f"{stats['symbol']} {stats['emission']:,.2f}[/{COLOR_PALETTE.P.EMISSION}]\n"
729
+ f"Stake: [{COLOR_PALETTE.S.MESH}]"
730
+ f"{stats['symbol']} {stats['stake']:,.2f}[/{COLOR_PALETTE.S.MESH}]"
731
+ )
732
+
733
+ console.print(stats_text)