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.
- meshtensor_cli/__init__.py +22 -0
- meshtensor_cli/cli.py +10742 -0
- meshtensor_cli/doc_generation_helper.py +4 -0
- meshtensor_cli/src/__init__.py +1085 -0
- meshtensor_cli/src/commands/__init__.py +0 -0
- meshtensor_cli/src/commands/axon/__init__.py +0 -0
- meshtensor_cli/src/commands/axon/axon.py +132 -0
- meshtensor_cli/src/commands/crowd/__init__.py +0 -0
- meshtensor_cli/src/commands/crowd/contribute.py +621 -0
- meshtensor_cli/src/commands/crowd/contributors.py +200 -0
- meshtensor_cli/src/commands/crowd/create.py +783 -0
- meshtensor_cli/src/commands/crowd/dissolve.py +219 -0
- meshtensor_cli/src/commands/crowd/refund.py +233 -0
- meshtensor_cli/src/commands/crowd/update.py +418 -0
- meshtensor_cli/src/commands/crowd/utils.py +124 -0
- meshtensor_cli/src/commands/crowd/view.py +991 -0
- meshtensor_cli/src/commands/governance/__init__.py +0 -0
- meshtensor_cli/src/commands/governance/governance.py +794 -0
- meshtensor_cli/src/commands/liquidity/__init__.py +0 -0
- meshtensor_cli/src/commands/liquidity/liquidity.py +699 -0
- meshtensor_cli/src/commands/liquidity/utils.py +202 -0
- meshtensor_cli/src/commands/proxy.py +700 -0
- meshtensor_cli/src/commands/stake/__init__.py +0 -0
- meshtensor_cli/src/commands/stake/add.py +799 -0
- meshtensor_cli/src/commands/stake/auto_staking.py +306 -0
- meshtensor_cli/src/commands/stake/children_hotkeys.py +865 -0
- meshtensor_cli/src/commands/stake/claim.py +770 -0
- meshtensor_cli/src/commands/stake/list.py +738 -0
- meshtensor_cli/src/commands/stake/move.py +1211 -0
- meshtensor_cli/src/commands/stake/remove.py +1466 -0
- meshtensor_cli/src/commands/stake/wizard.py +323 -0
- meshtensor_cli/src/commands/subnets/__init__.py +0 -0
- meshtensor_cli/src/commands/subnets/mechanisms.py +515 -0
- meshtensor_cli/src/commands/subnets/price.py +733 -0
- meshtensor_cli/src/commands/subnets/subnets.py +2908 -0
- meshtensor_cli/src/commands/sudo.py +1294 -0
- meshtensor_cli/src/commands/tc/__init__.py +0 -0
- meshtensor_cli/src/commands/tc/tc.py +190 -0
- meshtensor_cli/src/commands/treasury/__init__.py +0 -0
- meshtensor_cli/src/commands/treasury/treasury.py +194 -0
- meshtensor_cli/src/commands/view.py +354 -0
- meshtensor_cli/src/commands/wallets.py +2311 -0
- meshtensor_cli/src/commands/weights.py +467 -0
- meshtensor_cli/src/meshtensor/__init__.py +0 -0
- meshtensor_cli/src/meshtensor/balances.py +313 -0
- meshtensor_cli/src/meshtensor/chain_data.py +1263 -0
- meshtensor_cli/src/meshtensor/extrinsics/__init__.py +0 -0
- meshtensor_cli/src/meshtensor/extrinsics/mev_shield.py +174 -0
- meshtensor_cli/src/meshtensor/extrinsics/registration.py +1861 -0
- meshtensor_cli/src/meshtensor/extrinsics/root.py +550 -0
- meshtensor_cli/src/meshtensor/extrinsics/serving.py +255 -0
- meshtensor_cli/src/meshtensor/extrinsics/transfer.py +239 -0
- meshtensor_cli/src/meshtensor/meshtensor_interface.py +2598 -0
- meshtensor_cli/src/meshtensor/minigraph.py +254 -0
- meshtensor_cli/src/meshtensor/networking.py +12 -0
- meshtensor_cli/src/meshtensor/templates/main-filters.j2 +24 -0
- meshtensor_cli/src/meshtensor/templates/main-header.j2 +36 -0
- meshtensor_cli/src/meshtensor/templates/neuron-details.j2 +111 -0
- meshtensor_cli/src/meshtensor/templates/price-multi.j2 +113 -0
- meshtensor_cli/src/meshtensor/templates/price-single.j2 +99 -0
- meshtensor_cli/src/meshtensor/templates/subnet-details-header.j2 +49 -0
- meshtensor_cli/src/meshtensor/templates/subnet-details.j2 +32 -0
- meshtensor_cli/src/meshtensor/templates/subnet-metrics.j2 +57 -0
- meshtensor_cli/src/meshtensor/templates/subnets-table.j2 +28 -0
- meshtensor_cli/src/meshtensor/templates/table.j2 +267 -0
- meshtensor_cli/src/meshtensor/templates/view.css +1058 -0
- meshtensor_cli/src/meshtensor/templates/view.j2 +43 -0
- meshtensor_cli/src/meshtensor/templates/view.js +1053 -0
- meshtensor_cli/src/meshtensor/utils.py +2007 -0
- meshtensor_cli/version.py +23 -0
- meshtensor_cli-9.18.1.dist-info/METADATA +261 -0
- meshtensor_cli-9.18.1.dist-info/RECORD +74 -0
- meshtensor_cli-9.18.1.dist-info/WHEEL +4 -0
- 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)
|