bittensor-cli 8.4.2__py3-none-any.whl → 9.0.0rc1__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.
- bittensor_cli/__init__.py +2 -2
- bittensor_cli/cli.py +1503 -1372
- bittensor_cli/src/__init__.py +625 -197
- bittensor_cli/src/bittensor/balances.py +41 -8
- bittensor_cli/src/bittensor/chain_data.py +557 -428
- bittensor_cli/src/bittensor/extrinsics/registration.py +161 -47
- bittensor_cli/src/bittensor/extrinsics/root.py +14 -8
- bittensor_cli/src/bittensor/extrinsics/transfer.py +14 -21
- bittensor_cli/src/bittensor/minigraph.py +46 -8
- bittensor_cli/src/bittensor/subtensor_interface.py +572 -253
- bittensor_cli/src/bittensor/utils.py +326 -75
- bittensor_cli/src/commands/stake/__init__.py +154 -0
- bittensor_cli/src/commands/stake/children_hotkeys.py +123 -91
- bittensor_cli/src/commands/stake/move.py +1000 -0
- bittensor_cli/src/commands/stake/stake.py +1637 -1264
- bittensor_cli/src/commands/subnets/__init__.py +0 -0
- bittensor_cli/src/commands/subnets/price.py +867 -0
- bittensor_cli/src/commands/subnets/subnets.py +2043 -0
- bittensor_cli/src/commands/sudo.py +529 -26
- bittensor_cli/src/commands/wallets.py +231 -535
- bittensor_cli/src/commands/weights.py +15 -11
- {bittensor_cli-8.4.2.dist-info → bittensor_cli-9.0.0rc1.dist-info}/METADATA +7 -4
- bittensor_cli-9.0.0rc1.dist-info/RECORD +32 -0
- bittensor_cli/src/bittensor/async_substrate_interface.py +0 -2748
- bittensor_cli/src/commands/root.py +0 -1752
- bittensor_cli/src/commands/subnets.py +0 -897
- bittensor_cli-8.4.2.dist-info/RECORD +0 -31
- {bittensor_cli-8.4.2.dist-info → bittensor_cli-9.0.0rc1.dist-info}/WHEEL +0 -0
- {bittensor_cli-8.4.2.dist-info → bittensor_cli-9.0.0rc1.dist-info}/entry_points.txt +0 -0
- {bittensor_cli-8.4.2.dist-info → bittensor_cli-9.0.0rc1.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
|
+
)
|