bittensor-cli 8.4.3__py3-none-any.whl → 9.0.0__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 (33) hide show
  1. bittensor_cli/__init__.py +1 -1
  2. bittensor_cli/cli.py +1827 -1392
  3. bittensor_cli/src/__init__.py +623 -168
  4. bittensor_cli/src/bittensor/balances.py +41 -8
  5. bittensor_cli/src/bittensor/chain_data.py +557 -428
  6. bittensor_cli/src/bittensor/extrinsics/registration.py +129 -23
  7. bittensor_cli/src/bittensor/extrinsics/root.py +3 -3
  8. bittensor_cli/src/bittensor/extrinsics/transfer.py +6 -11
  9. bittensor_cli/src/bittensor/minigraph.py +46 -8
  10. bittensor_cli/src/bittensor/subtensor_interface.py +567 -250
  11. bittensor_cli/src/bittensor/utils.py +399 -25
  12. bittensor_cli/src/commands/stake/__init__.py +154 -0
  13. bittensor_cli/src/commands/stake/add.py +625 -0
  14. bittensor_cli/src/commands/stake/children_hotkeys.py +103 -75
  15. bittensor_cli/src/commands/stake/list.py +687 -0
  16. bittensor_cli/src/commands/stake/move.py +1000 -0
  17. bittensor_cli/src/commands/stake/remove.py +1146 -0
  18. bittensor_cli/src/commands/subnets/__init__.py +0 -0
  19. bittensor_cli/src/commands/subnets/price.py +867 -0
  20. bittensor_cli/src/commands/subnets/subnets.py +2028 -0
  21. bittensor_cli/src/commands/sudo.py +554 -12
  22. bittensor_cli/src/commands/wallets.py +225 -531
  23. bittensor_cli/src/commands/weights.py +2 -2
  24. {bittensor_cli-8.4.3.dist-info → bittensor_cli-9.0.0.dist-info}/METADATA +7 -4
  25. bittensor_cli-9.0.0.dist-info/RECORD +34 -0
  26. bittensor_cli/src/bittensor/async_substrate_interface.py +0 -2748
  27. bittensor_cli/src/commands/root.py +0 -1752
  28. bittensor_cli/src/commands/stake/stake.py +0 -1448
  29. bittensor_cli/src/commands/subnets.py +0 -897
  30. bittensor_cli-8.4.3.dist-info/RECORD +0 -31
  31. {bittensor_cli-8.4.3.dist-info → bittensor_cli-9.0.0.dist-info}/WHEEL +0 -0
  32. {bittensor_cli-8.4.3.dist-info → bittensor_cli-9.0.0.dist-info}/entry_points.txt +0 -0
  33. {bittensor_cli-8.4.3.dist-info → bittensor_cli-9.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,867 @@
1
+ import asyncio
2
+ import json
3
+ import math
4
+ from pywry import PyWry
5
+ from typing import TYPE_CHECKING
6
+
7
+ import plotille
8
+ import plotly.graph_objects as go
9
+
10
+ from bittensor_cli.src import COLOR_PALETTE
11
+ from bittensor_cli.src.bittensor.utils import (
12
+ console,
13
+ err_console,
14
+ get_subnet_name,
15
+ print_error,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
20
+
21
+
22
+ async def price(
23
+ subtensor: "SubtensorInterface",
24
+ netuids: list[int],
25
+ all_netuids: bool = False,
26
+ interval_hours: int = 24,
27
+ html_output: bool = False,
28
+ log_scale: bool = False,
29
+ ):
30
+ """
31
+ Fetch historical price data for subnets and display it in a chart.
32
+ """
33
+ if all_netuids:
34
+ netuids = [nid for nid in await subtensor.get_all_subnet_netuids() if nid != 0]
35
+
36
+ blocks_per_hour = int(3600 / 12) # ~300 blocks per hour
37
+ total_blocks = blocks_per_hour * interval_hours
38
+
39
+ with console.status(":chart_increasing: Fetching historical price data..."):
40
+ current_block_hash = await subtensor.substrate.get_chain_head()
41
+ current_block = await subtensor.substrate.get_block_number(current_block_hash)
42
+
43
+ step = 300
44
+ start_block = max(0, current_block - total_blocks)
45
+ block_numbers = list(range(start_block, current_block + 1, step))
46
+
47
+ # Block hashes
48
+ block_hash_cors = [
49
+ subtensor.substrate.get_block_hash(bn) for bn in block_numbers
50
+ ]
51
+ block_hashes = await asyncio.gather(*block_hash_cors)
52
+
53
+ # We fetch all subnets when there is more than one netuid
54
+ if all_netuids or len(netuids) > 1:
55
+ subnet_info_cors = [subtensor.all_subnets(bh) for bh in block_hashes]
56
+ else:
57
+ # If there is only one netuid, we fetch the subnet info for that netuid
58
+ netuid = netuids[0]
59
+ subnet_info_cors = [subtensor.subnet(netuid, bh) for bh in block_hashes]
60
+ all_subnet_infos = await asyncio.gather(*subnet_info_cors)
61
+
62
+ subnet_data = _process_subnet_data(
63
+ block_numbers, all_subnet_infos, netuids, all_netuids, interval_hours
64
+ )
65
+
66
+ if not subnet_data:
67
+ err_console.print("[red]No valid price data found for any subnet[/red]")
68
+ return
69
+
70
+ if html_output:
71
+ await _generate_html_output(
72
+ subnet_data, block_numbers, interval_hours, log_scale
73
+ )
74
+ else:
75
+ _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale)
76
+
77
+
78
+ def _process_subnet_data(
79
+ block_numbers,
80
+ all_subnet_infos,
81
+ netuids,
82
+ all_netuids,
83
+ interval_hours,
84
+ ):
85
+ """
86
+ Process subnet data into a structured format for price analysis.
87
+ """
88
+ subnet_data = {}
89
+ if all_netuids or len(netuids) > 1:
90
+ for netuid in netuids:
91
+ prices = []
92
+ valid_subnet_infos = []
93
+ for _, subnet_infos in zip(block_numbers, all_subnet_infos):
94
+ subnet_info = next(
95
+ (s for s in subnet_infos if s.netuid == netuid), None
96
+ )
97
+ if subnet_info:
98
+ prices.append(subnet_info.price.tao)
99
+ valid_subnet_infos.append(subnet_info)
100
+
101
+ if not valid_subnet_infos or not prices:
102
+ # No valid data found for this netuid
103
+ continue
104
+
105
+ if len(prices) < 5:
106
+ err_console.print(
107
+ f"[red]Insufficient price data for subnet {netuid}. "
108
+ f"Need at least 5 data points but only found {len(prices)}.[/red]"
109
+ )
110
+ continue
111
+
112
+ # Most recent data for statistics
113
+ latest_subnet_data = valid_subnet_infos[-1]
114
+ stats = {
115
+ "current_price": prices[-1],
116
+ "high": max(prices),
117
+ "low": min(prices),
118
+ "change_pct": ((prices[-1] - prices[0]) / prices[0] * 100),
119
+ "supply": latest_subnet_data.alpha_in.tao
120
+ + latest_subnet_data.alpha_out.tao,
121
+ "market_cap": latest_subnet_data.price.tao
122
+ * (latest_subnet_data.alpha_in.tao + latest_subnet_data.alpha_out.tao),
123
+ "emission": latest_subnet_data.emission.tao,
124
+ "stake": latest_subnet_data.alpha_out.tao,
125
+ "symbol": latest_subnet_data.symbol,
126
+ "name": get_subnet_name(latest_subnet_data),
127
+ }
128
+ subnet_data[netuid] = {
129
+ "prices": prices,
130
+ "stats": stats,
131
+ }
132
+
133
+ else:
134
+ prices = []
135
+ valid_subnet_infos = []
136
+ for _, subnet_info in zip(block_numbers, all_subnet_infos):
137
+ if subnet_info:
138
+ prices.append(subnet_info.price.tao)
139
+ valid_subnet_infos.append(subnet_info)
140
+
141
+ if not valid_subnet_infos or not prices:
142
+ err_console.print("[red]No valid price data found for any subnet[/red]")
143
+ return {}
144
+
145
+ if len(prices) < 5:
146
+ err_console.print(
147
+ f"[red]Insufficient price data for subnet {netuids[0]}. "
148
+ f"Need at least 5 data points but only found {len(prices)}.[/red]"
149
+ )
150
+ return {}
151
+
152
+ # Most recent data for statistics
153
+ latest_subnet_data = valid_subnet_infos[-1]
154
+ stats = {
155
+ "current_price": prices[-1],
156
+ "high": max(prices),
157
+ "low": min(prices),
158
+ "change_pct": ((prices[-1] - prices[0]) / prices[0] * 100),
159
+ "supply": latest_subnet_data.alpha_in.tao
160
+ + latest_subnet_data.alpha_out.tao,
161
+ "market_cap": latest_subnet_data.price.tao
162
+ * (latest_subnet_data.alpha_in.tao + latest_subnet_data.alpha_out.tao),
163
+ "emission": latest_subnet_data.emission.tao,
164
+ "stake": latest_subnet_data.alpha_out.tao,
165
+ "symbol": latest_subnet_data.symbol,
166
+ "name": get_subnet_name(latest_subnet_data),
167
+ }
168
+ subnet_data[netuids[0]] = {
169
+ "prices": prices,
170
+ "stats": stats,
171
+ }
172
+
173
+ # Sort results by market cap
174
+ sorted_subnet_data = dict(
175
+ sorted(
176
+ subnet_data.items(),
177
+ key=lambda x: x[1]["stats"]["market_cap"],
178
+ reverse=True,
179
+ )
180
+ )
181
+ return sorted_subnet_data
182
+
183
+
184
+ def _generate_html_single_subnet(
185
+ netuid,
186
+ data,
187
+ block_numbers,
188
+ interval_hours,
189
+ log_scale,
190
+ ):
191
+ """
192
+ Generate an HTML chart for a single subnet.
193
+ """
194
+ stats = data["stats"]
195
+ prices = data["prices"]
196
+
197
+ fig = go.Figure()
198
+ fig.add_trace(
199
+ go.Scatter(
200
+ x=block_numbers,
201
+ y=prices,
202
+ mode="lines",
203
+ name=f"Subnet {netuid} - {stats['name']}"
204
+ if stats["name"]
205
+ else f"Subnet {netuid}",
206
+ line=dict(width=2, color="#50C878"),
207
+ )
208
+ )
209
+
210
+ fig.update_layout(
211
+ template="plotly_dark",
212
+ paper_bgcolor="#000000",
213
+ plot_bgcolor="#000000",
214
+ font=dict(color="white"),
215
+ showlegend=True,
216
+ legend=dict(
217
+ x=1.02,
218
+ y=1.0,
219
+ xanchor="left",
220
+ yanchor="top",
221
+ bgcolor="rgba(0,0,0,0)",
222
+ bordercolor="rgba(255,255,255,0.2)",
223
+ borderwidth=1,
224
+ ),
225
+ margin=dict(t=160, r=50, b=50, l=50),
226
+ height=600,
227
+ )
228
+
229
+ price_title = f"Price ({stats['symbol']})"
230
+ if log_scale:
231
+ price_title += " Log Scale"
232
+
233
+ # Label axes
234
+ fig.update_xaxes(
235
+ title="Block",
236
+ gridcolor="rgba(128,128,128,0.2)",
237
+ zerolinecolor="rgba(128,128,128,0.2)",
238
+ type="log" if log_scale else "linear",
239
+ )
240
+ fig.update_yaxes(
241
+ title=price_title,
242
+ gridcolor="rgba(128,128,128,0.2)",
243
+ zerolinecolor="rgba(128,128,128,0.2)",
244
+ type="log" if log_scale else "linear",
245
+ )
246
+
247
+ # Price change color
248
+ price_change_class = "text-green" if stats["change_pct"] > 0 else "text-red"
249
+ # Change sign
250
+ sign_icon = "▲" if stats["change_pct"] > 0 else "▼"
251
+
252
+ fig_dict = fig.to_dict()
253
+ fig_json = json.dumps(fig_dict)
254
+ html_content = f"""
255
+ <!DOCTYPE html>
256
+ <html lang="en">
257
+ <head>
258
+ <meta charset="UTF-8">
259
+ <title>Subnet Price View</title>
260
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
261
+ <style>
262
+ body {{
263
+ background-color: #000;
264
+ color: #fff;
265
+ font-family: Arial, sans-serif;
266
+ margin: 0;
267
+ padding: 20px;
268
+ }}
269
+ .header-container {{
270
+ display: flex;
271
+ align-items: flex-start;
272
+ justify-content: space-between;
273
+ margin-bottom: 20px;
274
+ }}
275
+ .price-info {{
276
+ max-width: 60%;
277
+ }}
278
+ .main-price {{
279
+ font-size: 36px;
280
+ font-weight: 600;
281
+ margin-bottom: 5px;
282
+ }}
283
+ .price-change {{
284
+ font-size: 18px;
285
+ margin-left: 8px;
286
+ font-weight: 500;
287
+ }}
288
+ .text-green {{ color: #00FF00; }}
289
+ .text-red {{ color: #FF5555; }}
290
+ .text-blue {{ color: #87CEEB; }}
291
+ .text-steel {{ color: #4682B4; }}
292
+ .text-purple{{ color: #DDA0DD; }}
293
+ .text-gold {{ color: #FFD700; }}
294
+
295
+ .sub-stats-row {{
296
+ display: flex;
297
+ flex-wrap: wrap;
298
+ margin-top: 10px;
299
+ }}
300
+ .stat-item {{
301
+ margin-right: 20px;
302
+ margin-bottom: 6px;
303
+ font-size: 14px;
304
+ }}
305
+ .side-stats {{
306
+ min-width: 220px;
307
+ display: flex;
308
+ flex-direction: column;
309
+ align-items: flex-start;
310
+ }}
311
+ .side-stats div {{
312
+ margin-bottom: 6px;
313
+ font-size: 14px;
314
+ }}
315
+ #chart-container {{
316
+ margin-top: 20px;
317
+ width: 100%;
318
+ height: 600px;
319
+ }}
320
+ </style>
321
+ </head>
322
+ <body>
323
+ <div class="header-container">
324
+ <div class="price-info">
325
+ <div class="main-price">
326
+ {stats['current_price']:.6f} {stats['symbol']}
327
+ <span class="price-change {price_change_class}">
328
+ {sign_icon} {abs(stats['change_pct']):.2f}%
329
+ </span>
330
+ </div>
331
+ <div class="sub-stats-row">
332
+ <div class="stat-item">
333
+ {interval_hours}h High: <span class="text-green">{stats['high']:.6f} {stats['symbol']}</span>
334
+ </div>
335
+ <div class="stat-item">
336
+ {interval_hours}h Low: <span class="text-red">{stats['low']:.6f} {stats['symbol']}</span>
337
+ </div>
338
+ </div>
339
+ </div>
340
+ <div class="side-stats">
341
+ <div>Supply: <span class="text-blue">{stats['supply']:.2f} {stats['symbol']}</span></div>
342
+ <div>Market Cap: <span class="text-steel">{stats['market_cap']:.2f} τ</span></div>
343
+ <div>Emission: <span class="text-purple">{stats['emission']:.2f} {stats['symbol']}</span></div>
344
+ <div>Stake: <span class="text-gold">{stats['stake']:.2f} {stats['symbol']}</span></div>
345
+ </div>
346
+ </div>
347
+ <div id="chart-container"></div>
348
+ <script>
349
+ var figData = {fig_json};
350
+ Plotly.newPlot('chart-container', figData.data, figData.layout);
351
+ </script>
352
+ </body>
353
+ </html>
354
+ """
355
+
356
+ return html_content
357
+
358
+
359
+ def _generate_html_multi_subnet(subnet_data, block_numbers, interval_hours, log_scale):
360
+ """
361
+ Generate an HTML chart for multiple subnets.
362
+ """
363
+ # Pick top subnet by market cap
364
+ top_subnet_netuid = max(
365
+ subnet_data.keys(),
366
+ key=lambda k: subnet_data[k]["stats"]["market_cap"],
367
+ )
368
+ top_subnet_stats = subnet_data[top_subnet_netuid]["stats"]
369
+
370
+ fig = go.Figure()
371
+ fig.update_layout(
372
+ template="plotly_dark",
373
+ paper_bgcolor="#000000",
374
+ plot_bgcolor="#000000",
375
+ font=dict(color="white"),
376
+ showlegend=True,
377
+ legend=dict(
378
+ x=1.02,
379
+ y=1.0,
380
+ xanchor="left",
381
+ yanchor="top",
382
+ bgcolor="rgba(0,0,0,0)",
383
+ bordercolor="rgba(255,255,255,0.2)",
384
+ borderwidth=1,
385
+ ),
386
+ margin=dict(t=200, r=80, b=50, l=50),
387
+ height=700,
388
+ )
389
+
390
+ price_title = "Price (τ)"
391
+ if log_scale:
392
+ price_title += " Log Scale"
393
+
394
+ # Label axes
395
+ fig.update_xaxes(
396
+ title="Block",
397
+ gridcolor="rgba(128,128,128,0.2)",
398
+ zerolinecolor="rgba(128,128,128,0.2)",
399
+ type="log" if log_scale else "linear",
400
+ )
401
+ fig.update_yaxes(
402
+ title=price_title,
403
+ gridcolor="rgba(128,128,128,0.2)",
404
+ zerolinecolor="rgba(128,128,128,0.2)",
405
+ type="log" if log_scale else "linear",
406
+ )
407
+
408
+ # Create annotation for top subnet
409
+ sign_icon = "▲" if top_subnet_stats["change_pct"] > 0 else "▼"
410
+ change_color = "#00FF00" if top_subnet_stats["change_pct"] > 0 else "#FF5555"
411
+
412
+ left_text = (
413
+ f"Top subnet: Subnet {top_subnet_netuid}"
414
+ + (f" - {top_subnet_stats['name']}" if top_subnet_stats["name"] else "")
415
+ + "<br><br>"
416
+ + f"<span style='font-size: 24px'>{top_subnet_stats['current_price']:.6f} {top_subnet_stats['symbol']}"
417
+ + f"<span style='color: {change_color}'> {sign_icon} {abs(top_subnet_stats['change_pct']):.2f}%</span></span><br><br>"
418
+ + f"{interval_hours}h High: <span style='color: #00FF00'>{top_subnet_stats['high']:.6f}</span>, "
419
+ + f"Low: <span style='color: #FF5555'>{top_subnet_stats['low']:.6f}</span>"
420
+ )
421
+
422
+ right_text = (
423
+ f"Supply: <span style='color: #87CEEB'>{top_subnet_stats['supply']:.2f} {top_subnet_stats['symbol']}</span><br>"
424
+ f"Market Cap: <span style='color: #4682B4'>{top_subnet_stats['market_cap']:.2f} τ</span><br>"
425
+ f"Emission: <span style='color: #DDA0DD'>{top_subnet_stats['emission']:.2f} {top_subnet_stats['symbol']}</span><br>"
426
+ f"Stake: <span style='color: #FFD700'>{top_subnet_stats['stake']:.2f} {top_subnet_stats['symbol']}</span>"
427
+ )
428
+
429
+ all_annotations = [
430
+ dict(
431
+ text=left_text,
432
+ x=0.0,
433
+ y=1.3,
434
+ xref="paper",
435
+ yref="paper",
436
+ align="left",
437
+ showarrow=False,
438
+ font=dict(size=14),
439
+ xanchor="left",
440
+ yanchor="top",
441
+ ),
442
+ dict(
443
+ text=right_text,
444
+ x=1.02,
445
+ y=1.3,
446
+ xref="paper",
447
+ yref="paper",
448
+ align="left",
449
+ showarrow=False,
450
+ font=dict(size=14),
451
+ xanchor="left",
452
+ yanchor="top",
453
+ ),
454
+ ]
455
+
456
+ fig.update_layout(annotations=all_annotations)
457
+
458
+ # Generate colors for subnets
459
+ def generate_color_palette(n):
460
+ """Generate n distinct colors using a variation of HSV color space."""
461
+ colors = []
462
+ for i in range(n):
463
+ hue = i * 0.618033988749895 % 1
464
+ saturation = 0.6 + (i % 3) * 0.2
465
+ value = 0.8 + (i % 2) * 0.2 # Brightness
466
+
467
+ h = hue * 6
468
+ c = value * saturation
469
+ x = c * (1 - abs(h % 2 - 1))
470
+ m = value - c
471
+
472
+ if h < 1:
473
+ r, g, b = c, x, 0
474
+ elif h < 2:
475
+ r, g, b = x, c, 0
476
+ elif h < 3:
477
+ r, g, b = 0, c, x
478
+ elif h < 4:
479
+ r, g, b = 0, x, c
480
+ elif h < 5:
481
+ r, g, b = x, 0, c
482
+ else:
483
+ r, g, b = c, 0, x
484
+
485
+ rgb = (
486
+ int((r + m) * 255),
487
+ int((g + m) * 255),
488
+ int((b + m) * 255),
489
+ )
490
+ colors.append(f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}")
491
+ return colors
492
+
493
+ base_colors = generate_color_palette(len(subnet_data) + 1)
494
+
495
+ # Plot each subnet as a separate trace
496
+ subnet_keys = list(subnet_data.keys())
497
+ for i, netuid in enumerate(subnet_keys):
498
+ d = subnet_data[netuid]
499
+ fig.add_trace(
500
+ go.Scatter(
501
+ x=block_numbers,
502
+ y=d["prices"],
503
+ mode="lines",
504
+ name=(
505
+ f"Subnet {netuid} - {d['stats']['name']}"
506
+ if d["stats"]["name"]
507
+ else f"Subnet {netuid}"
508
+ ),
509
+ line=dict(width=2, color=base_colors[i]),
510
+ visible=True,
511
+ )
512
+ )
513
+
514
+ # Annotations for each subnet
515
+ def build_single_subnet_annotations(netuid):
516
+ s = subnet_data[netuid]["stats"]
517
+ name_line = f"Subnet {netuid}" + (f" - {s['name']}" if s["name"] else "")
518
+
519
+ sign_icon = "▲" if s["change_pct"] > 0 else "▼"
520
+ change_color = "#00FF00" if s["change_pct"] > 0 else "#FF5555"
521
+
522
+ left_text = (
523
+ f"{name_line}<br><br>"
524
+ f"<span style='font-size: 24px'>{s['current_price']:.6f} {s['symbol']}"
525
+ f"<span style='color: {change_color}'> {sign_icon} {abs(s['change_pct']):.2f}%</span></span><br><br>"
526
+ f"{interval_hours}h High: <span style='color: #00FF00'>{s['high']:.6f}</span>, "
527
+ f"Low: <span style='color: #FF5555'>{s['low']:.6f}</span>"
528
+ )
529
+
530
+ right_text = (
531
+ f"Supply: <span style='color: #87CEEB'>{s['supply']:.2f} {s['symbol']}</span><br>"
532
+ f"Market Cap: <span style='color: #4682B4'>{s['market_cap']:.2f} τ</span><br>"
533
+ f"Emission: <span style='color: #DDA0DD'>{s['emission']:.2f} {s['symbol']}</span><br>"
534
+ f"Stake: <span style='color: #FFD700'>{s['stake']:.2f} {s['symbol']}</span>"
535
+ )
536
+
537
+ left_annot = dict(
538
+ text=left_text,
539
+ x=0.0,
540
+ y=1.3,
541
+ xref="paper",
542
+ yref="paper",
543
+ align="left",
544
+ showarrow=False,
545
+ font=dict(size=14),
546
+ xanchor="left",
547
+ yanchor="top",
548
+ )
549
+ right_annot = dict(
550
+ text=right_text,
551
+ x=1.02,
552
+ y=1.3,
553
+ xref="paper",
554
+ yref="paper",
555
+ align="left",
556
+ showarrow=False,
557
+ font=dict(size=14),
558
+ xanchor="left",
559
+ yanchor="top",
560
+ )
561
+ return [left_annot, right_annot]
562
+
563
+ # "All" visibility mask
564
+ all_visibility = [True] * len(subnet_keys)
565
+
566
+ # Build visibility masks for each subnet
567
+ subnet_modes = {}
568
+ for idx, netuid in enumerate(subnet_keys):
569
+ single_vis = [False] * len(subnet_keys)
570
+ single_vis[idx] = True
571
+ single_annots = build_single_subnet_annotations(netuid)
572
+ subnet_modes[netuid] = {
573
+ "visible": single_vis,
574
+ "annotations": single_annots,
575
+ }
576
+
577
+ fig_json = fig.to_json()
578
+ all_visibility_json = json.dumps(all_visibility)
579
+ all_annotations_json = json.dumps(all_annotations)
580
+
581
+ subnet_modes_json = {}
582
+ for netuid, mode_data in subnet_modes.items():
583
+ subnet_modes_json[netuid] = {
584
+ "visible": json.dumps(mode_data["visible"]),
585
+ "annotations": json.dumps(mode_data["annotations"]),
586
+ }
587
+
588
+ # We sort netuids by market cap but for buttons, they are ordered by netuid
589
+ sorted_subnet_keys = sorted(subnet_data.keys())
590
+ all_button_html = (
591
+ '<button class="subnet-button active" onclick="setAll()">All</button>'
592
+ )
593
+ subnet_buttons_html = ""
594
+ for netuid in sorted_subnet_keys:
595
+ subnet_buttons_html += f'<button class="subnet-button" onclick="setSubnet({netuid})">S{netuid}</button> '
596
+
597
+ html_content = f"""
598
+ <!DOCTYPE html>
599
+ <html>
600
+ <head>
601
+ <title>Multi-Subnet Price Chart</title>
602
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
603
+ <style>
604
+ body {{
605
+ background-color: #000;
606
+ color: #fff;
607
+ font-family: Arial, sans-serif;
608
+ margin: 0;
609
+ padding: 20px;
610
+ }}
611
+ .container {{
612
+ display: flex;
613
+ flex-direction: column;
614
+ gap: 60px;
615
+ }}
616
+ #multi-subnet-chart {{
617
+ width: 90vw;
618
+ height: 70vh;
619
+ margin-bottom: 40px;
620
+ }}
621
+ .subnet-buttons {{
622
+ display: grid;
623
+ grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
624
+ gap: 8px;
625
+ max-height: 120px;
626
+ overflow-y: auto;
627
+ padding-right: 10px;
628
+ margin-top: 50px;
629
+ border-top: 1px solid rgba(255,255,255,0.1);
630
+ padding-top: 50px;
631
+ position: relative;
632
+ bottom: 0;
633
+ }}
634
+ .subnet-buttons::-webkit-scrollbar {{
635
+ width: 8px;
636
+ }}
637
+ .subnet-buttons::-webkit-scrollbar-track {{
638
+ background: rgba(50,50,50,0.3);
639
+ border-radius: 4px;
640
+ }}
641
+ .subnet-buttons::-webkit-scrollbar-thumb {{
642
+ background: rgba(100,100,100,0.8);
643
+ border-radius: 4px;
644
+ }}
645
+ .subnet-button {{
646
+ background-color: rgba(50,50,50,0.8);
647
+ border: 1px solid rgba(70,70,70,0.9);
648
+ color: white;
649
+ padding: 8px 16px;
650
+ cursor: pointer;
651
+ border-radius: 4px;
652
+ font-size: 14px;
653
+ transition: background-color 0.2s;
654
+ white-space: nowrap;
655
+ overflow: hidden;
656
+ text-overflow: ellipsis;
657
+ }}
658
+ .subnet-button:hover {{
659
+ background-color: rgba(70,70,70,0.9);
660
+ }}
661
+ .subnet-button.active {{
662
+ background-color: rgba(100,100,100,0.9);
663
+ border-color: rgba(120,120,120,1);
664
+ }}
665
+ </style>
666
+ </head>
667
+ <body>
668
+ <div class="container">
669
+ <div id="multi-subnet-chart"></div>
670
+ <div class="subnet-buttons">
671
+ {all_button_html}
672
+ {subnet_buttons_html}
673
+ </div>
674
+ </div>
675
+ <script>
676
+ var figData = {fig_json};
677
+ var allVisibility = {all_visibility_json};
678
+ var allAnnotations = {all_annotations_json};
679
+
680
+ var subnetModes = {json.dumps(subnet_modes_json)};
681
+ // parse back to arrays/objects
682
+ for (var netuid in subnetModes) {{
683
+ subnetModes[netuid].visible = JSON.parse(subnetModes[netuid].visible);
684
+ subnetModes[netuid].annotations = JSON.parse(subnetModes[netuid].annotations);
685
+ }}
686
+
687
+ Plotly.newPlot('multi-subnet-chart', figData.data, figData.layout);
688
+
689
+ function clearActiveButtons() {{
690
+ document.querySelectorAll('.subnet-button').forEach(btn => btn.classList.remove('active'));
691
+ }}
692
+
693
+ function setAll() {{
694
+ clearActiveButtons();
695
+ event.currentTarget.classList.add('active');
696
+ Plotly.update('multi-subnet-chart',
697
+ {{visible: allVisibility}},
698
+ {{annotations: allAnnotations}}
699
+ );
700
+ }}
701
+
702
+ function setSubnet(netuid) {{
703
+ clearActiveButtons();
704
+ event.currentTarget.classList.add('active');
705
+ var mode = subnetModes[netuid];
706
+ Plotly.update('multi-subnet-chart',
707
+ {{visible: mode.visible}},
708
+ {{annotations: mode.annotations}}
709
+ );
710
+ }}
711
+ </script>
712
+ </body>
713
+ </html>
714
+ """
715
+ return html_content
716
+
717
+
718
+ async def _generate_html_output(
719
+ subnet_data,
720
+ block_numbers,
721
+ interval_hours,
722
+ log_scale: bool = False,
723
+ ):
724
+ """
725
+ Start PyWry and display the price chart in a window.
726
+ """
727
+ try:
728
+ subnet_keys = list(subnet_data.keys())
729
+
730
+ # Single subnet
731
+ if len(subnet_keys) == 1:
732
+ netuid = subnet_keys[0]
733
+ data = subnet_data[netuid]
734
+ html_content = _generate_html_single_subnet(
735
+ netuid, data, block_numbers, interval_hours, log_scale
736
+ )
737
+ title = f"Subnet {netuid} Price View"
738
+ else:
739
+ # Multi-subnet
740
+ html_content = _generate_html_multi_subnet(
741
+ subnet_data, block_numbers, interval_hours, log_scale
742
+ )
743
+ title = "Subnets Price Chart"
744
+ console.print(
745
+ "[dark_sea_green3]Opening price chart in a window. Press Ctrl+C to close.[/dark_sea_green3]"
746
+ )
747
+ handler = PyWry()
748
+ handler.send_html(
749
+ html=html_content,
750
+ title=title,
751
+ width=1200,
752
+ height=800,
753
+ )
754
+ handler.start()
755
+ await asyncio.sleep(5)
756
+
757
+ # TODO: Improve this logic
758
+ try:
759
+ while True:
760
+ if _has_exited(handler):
761
+ break
762
+ await asyncio.sleep(1)
763
+ except KeyboardInterrupt:
764
+ pass
765
+ finally:
766
+ if not _has_exited(handler):
767
+ try:
768
+ handler.close()
769
+ except Exception:
770
+ pass
771
+ except Exception as e:
772
+ print_error(f"Error generating price chart: {e}")
773
+
774
+
775
+ def _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale):
776
+ """
777
+ Render the price data in a textual CLI style with plotille ASCII charts.
778
+ """
779
+ for netuid, data in subnet_data.items():
780
+ fig = plotille.Figure()
781
+ fig.width = 60
782
+ fig.height = 20
783
+ fig.color_mode = "rgb"
784
+ fig.background = None
785
+
786
+ def color_label(text):
787
+ return plotille.color(text, fg=(186, 233, 143), mode="rgb")
788
+
789
+ fig.x_label = color_label("Block")
790
+ y_label_text = f"Price ({data['stats']['symbol']})"
791
+ fig.y_label = color_label(y_label_text)
792
+
793
+ prices = data["prices"]
794
+ if log_scale:
795
+ prices = [math.log10(p) for p in prices]
796
+
797
+ fig.set_x_limits(min_=min(block_numbers), max_=max(block_numbers))
798
+ fig.set_y_limits(
799
+ min_=data["stats"]["low"] * 0.99,
800
+ max_=data["stats"]["high"] * 1.01,
801
+ )
802
+
803
+ fig.plot(
804
+ block_numbers,
805
+ data["prices"],
806
+ label=f"Subnet {netuid} Price",
807
+ interp="linear",
808
+ lc="bae98f",
809
+ )
810
+
811
+ stats = data["stats"]
812
+ change_color = "dark_sea_green3" if stats["change_pct"] > 0 else "red"
813
+
814
+ if netuid != 0:
815
+ console.print(
816
+ f"\n[{COLOR_PALETTE['GENERAL']['SYMBOL']}]Subnet {netuid} - {stats['symbol']} "
817
+ f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE['GENERAL']['SYMBOL']}]\n"
818
+ f"Current: [blue]{stats['current_price']:.6f}{stats['symbol']}[/blue]\n"
819
+ f"{interval_hours}h High: [dark_sea_green3]{stats['high']:.6f}{stats['symbol']}[/dark_sea_green3]\n"
820
+ f"{interval_hours}h Low: [red]{stats['low']:.6f}{stats['symbol']}[/red]\n"
821
+ f"{interval_hours}h Change: [{change_color}]{stats['change_pct']:.2f}%[/{change_color}]\n"
822
+ )
823
+ else:
824
+ console.print(
825
+ f"\n[{COLOR_PALETTE['GENERAL']['SYMBOL']}]Subnet {netuid} - {stats['symbol']} "
826
+ f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE['GENERAL']['SYMBOL']}]\n"
827
+ f"Current: [blue]{stats['symbol']} {stats['current_price']:.6f}[/blue]\n"
828
+ f"{interval_hours}h High: [dark_sea_green3]{stats['symbol']} {stats['high']:.6f}[/dark_sea_green3]\n"
829
+ f"{interval_hours}h Low: [red]{stats['symbol']} {stats['low']:.6f}[/red]\n"
830
+ f"{interval_hours}h Change: [{change_color}]{stats['change_pct']:.2f}%[/{change_color}]\n"
831
+ )
832
+
833
+ print(fig.show())
834
+
835
+ if netuid != 0:
836
+ stats_text = (
837
+ "\nLatest stats:\n"
838
+ f"Supply: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]"
839
+ f"{stats['supply']:,.2f} {stats['symbol']}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]\n"
840
+ f"Market Cap: [steel_blue3]{stats['market_cap']:,.2f} {stats['symbol']} / 21M[/steel_blue3]\n"
841
+ f"Emission: [{COLOR_PALETTE['POOLS']['EMISSION']}]"
842
+ f"{stats['emission']:,.2f} {stats['symbol']}[/{COLOR_PALETTE['POOLS']['EMISSION']}]\n"
843
+ f"Stake: [{COLOR_PALETTE['STAKE']['TAO']}]"
844
+ f"{stats['stake']:,.2f} {stats['symbol']}[/{COLOR_PALETTE['STAKE']['TAO']}]"
845
+ )
846
+ else:
847
+ stats_text = (
848
+ "\nLatest stats:\n"
849
+ f"Supply: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]"
850
+ f"{stats['symbol']} {stats['supply']:,.2f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]\n"
851
+ f"Market Cap: [steel_blue3]{stats['symbol']} {stats['market_cap']:,.2f} / 21M[/steel_blue3]\n"
852
+ f"Emission: [{COLOR_PALETTE['POOLS']['EMISSION']}]"
853
+ f"{stats['symbol']} {stats['emission']:,.2f}[/{COLOR_PALETTE['POOLS']['EMISSION']}]\n"
854
+ f"Stake: [{COLOR_PALETTE['STAKE']['TAO']}]"
855
+ f"{stats['symbol']} {stats['stake']:,.2f}[/{COLOR_PALETTE['STAKE']['TAO']}]"
856
+ )
857
+
858
+ console.print(stats_text)
859
+
860
+
861
+ def _has_exited(handler) -> bool:
862
+ """Check if PyWry process has cleanly exited with returncode 0."""
863
+ return (
864
+ hasattr(handler, "runner")
865
+ and handler.runner is not None
866
+ and handler.runner.returncode == 0
867
+ )