From deca5ba2ea867e898ff52c9fe6eadccd3aede6e9 Mon Sep 17 00:00:00 2001 From: Mobile-Crest Date: Wed, 3 Dec 2025 04:22:21 +0100 Subject: [PATCH 1/4] improve add liquidity logic - Closes #532 --- bittensor_cli/cli.py | 63 ++-- .../src/commands/liquidity/liquidity.py | 268 +++++++++++++++++- bittensor_cli/src/commands/liquidity/utils.py | 148 ++++++++++ tests/unit_tests/test_liquidity_utils.py | 166 +++++++++++ 4 files changed, 602 insertions(+), 43 deletions(-) create mode 100644 tests/unit_tests/test_liquidity_utils.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9ad6c89df..827afa4bd 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7365,6 +7365,18 @@ def liquidity_add( "--liquidity_price_high", help="High price for the adding liquidity position.", ), + tao_amount: Optional[float] = typer.Option( + None, + "--tao-amount", + "--tao_amount", + help="Amount of TAO to provide (for mixed range positions).", + ), + alpha_amount: Optional[float] = typer.Option( + None, + "--alpha-amount", + "--alpha_amount", + help="Amount of Alpha to provide (for mixed range positions).", + ), prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -7372,6 +7384,8 @@ def liquidity_add( ): """Add liquidity to the swap (as a combination of TAO + Alpha).""" self.verbosity_handler(quiet, verbose, json_output) + + # Step 1: Ask for netuid if not netuid: netuid = Prompt.ask( f"Enter the [{COLORS.G.SUBHEAD_MAIN}]netuid[/{COLORS.G.SUBHEAD_MAIN}] to use", @@ -7379,53 +7393,18 @@ def liquidity_add( show_default=False, ) - wallet, hotkey = self.wallet_ask( - wallet_name=wallet_name, - wallet_path=wallet_path, - wallet_hotkey=wallet_hotkey, - ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], - validate=WV.WALLET, - return_wallet_and_hotkey=True, - ) - # Determine the liquidity amount. - if liquidity_: - liquidity_ = Balance.from_tao(liquidity_) - else: - liquidity_ = prompt_liquidity("Enter the amount of liquidity") - - # Determine price range - if price_low: - price_low = Balance.from_tao(price_low) - else: - price_low = prompt_liquidity("Enter liquidity position low price") - - if price_high: - price_high = Balance.from_tao(price_high) - else: - price_high = prompt_liquidity( - "Enter liquidity position high price (must be greater than low price)" - ) - - if price_low >= price_high: - err_console.print("The low price must be lower than the high price.") - return False - logger.debug( - f"args:\n" - f"hotkey: {hotkey}\n" - f"netuid: {netuid}\n" - f"liquidity: {liquidity_}\n" - f"price_low: {price_low}\n" - f"price_high: {price_high}\n" - ) return self._run_command( - liquidity.add_liquidity( + liquidity.add_liquidity_interactive( subtensor=self.initialize_chain(network), - wallet=wallet, - hotkey_ss58=hotkey, + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, netuid=netuid, - liquidity=liquidity_, + liquidity_=liquidity_, price_low=price_low, price_high=price_high, + tao_amount=tao_amount, + alpha_amount=alpha_amount, prompt=prompt, json_output=json_output, ) diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index a262e8874..f96c29e96 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Optional from async_substrate_interface import AsyncExtrinsicReceipt -from rich.prompt import Confirm +from rich.prompt import Confirm, FloatPrompt, Prompt from rich.table import Column, Table from bittensor_cli.src import COLORS @@ -21,6 +21,9 @@ get_fees, price_to_tick, tick_to_price, + calculate_max_liquidity_from_balances, + calculate_alpha_from_tao, + calculate_tao_from_alpha, ) if TYPE_CHECKING: @@ -289,6 +292,269 @@ async def add_liquidity( return success, message +async def add_liquidity_interactive( + subtensor: "SubtensorInterface", + wallet_name: str, + wallet_path: str, + wallet_hotkey: str, + netuid: int, + liquidity_: Optional[float], + price_low: Optional[float], + price_high: Optional[float], + tao_amount: Optional[float], + alpha_amount: Optional[float], + prompt: bool, + json_output: bool, +) -> tuple[bool, str]: + """Interactive flow for adding liquidity based on the improved logic. + + Steps: + 1. Check if subnet exists + 2. Ask user to enter low and high position prices + 3. Fetch current SN price + 4. Based on price position: + - If low >= current: only ask for Alpha amount + - If high <= current: only ask for TAO amount + - Otherwise: calculate max liquidity and ask for TAO or Alpha amount + 5. Execute the extrinsic + """ + from bittensor_wallet import Wallet + from bittensor_cli.src.bittensor.utils import get_hotkey_pub_ss58 + import math + + # Load wallet + try: + wallet = Wallet(name=wallet_name, path=wallet_path, hotkey=wallet_hotkey) + except Exception as e: + return False, f"Failed to load wallet: {e}" + + # Step 2: Check if the subnet exists + if not await subtensor.subnet_exists(netuid=netuid): + return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." + + # Step 3: Ask user to enter low and high position prices + if price_low is None: + while True: + price_low_input = FloatPrompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the low price for the liquidity position[/{COLORS.G.SUBHEAD_MAIN}]" + ) + if price_low_input > 0: + price_low = price_low_input + break + console.print("[red]Price must be greater than 0[/red]") + + if price_high is None: + while True: + price_high_input = FloatPrompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the high price for the liquidity position[/{COLORS.G.SUBHEAD_MAIN}]" + ) + if price_high_input > price_low: + price_high = price_high_input + break + console.print(f"[red]High price must be greater than low price ({price_low})[/red]") + + price_low_balance = Balance.from_tao(price_low) + price_high_balance = Balance.from_tao(price_high) + + # Step 4: Fetch current SN price + with console.status(":satellite: Fetching current subnet price...", spinner="aesthetic"): + current_price = await subtensor.get_subnet_price(netuid=netuid) + + console.print(f"Current subnet price: [cyan]{current_price.tao:.6f} τ[/cyan]") + + # Determine hotkey to use (optional for some cases) + hotkey_ss58 = None + if wallet_hotkey: + hotkey_ss58 = get_hotkey_pub_ss58(wallet, wallet_hotkey) + + # Step 5: Determine which case we're in based on price position + liquidity_to_add = None + + # Case 1: Low price >= current price (only Alpha needed) + if price_low >= current_price.tao: + console.print( + f"\n[yellow]The low price ({price_low:.6f}) is higher than or equal to the current price ({current_price.tao:.6f}).[/yellow]" + ) + console.print("[yellow]Only Alpha tokens are needed for this position.[/yellow]\n") + + # Ask for hotkey if not provided + if hotkey_ss58 is None: + hotkey_input = Prompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the hotkey to take Alpha stake from (optional, press Enter to skip)[/{COLORS.G.SUBHEAD_MAIN}]", + default="" + ) + if hotkey_input: + hotkey_ss58 = get_hotkey_pub_ss58(wallet, hotkey_input) + else: + # Use default hotkey from wallet + hotkey_ss58 = wallet.hotkey.ss58_address + + # Ask for Alpha amount + if alpha_amount is None: + alpha_amount = FloatPrompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of Alpha to provide[/{COLORS.G.SUBHEAD_MAIN}]" + ) + + alpha_balance = Balance.from_tao(alpha_amount) + + # Calculate liquidity from Alpha + # L = alpha / (1/sqrt_price_low - 1/sqrt_price_high) + sqrt_price_low = math.sqrt(price_low) + sqrt_price_high = math.sqrt(price_high) + liquidity_to_add = Balance.from_rao( + int(alpha_balance.rao / (1 / sqrt_price_low - 1 / sqrt_price_high)) + ) + + # Case 2: High price <= current price (only TAO needed) + elif price_high <= current_price.tao: + console.print( + f"\n[yellow]The high price ({price_high:.6f}) is lower than or equal to the current price ({current_price.tao:.6f}).[/yellow]" + ) + console.print("[yellow]Only TAO tokens are needed for this position.[/yellow]\n") + + # Ask for hotkey if not provided (has no effect but required for extrinsic) + if hotkey_ss58 is None: + hotkey_input = Prompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the hotkey (optional, press Enter to use default)[/{COLORS.G.SUBHEAD_MAIN}]", + default="" + ) + if hotkey_input: + hotkey_ss58 = get_hotkey_pub_ss58(wallet, hotkey_input) + else: + hotkey_ss58 = wallet.hotkey.ss58_address + + # Ask for TAO amount + if tao_amount is None: + tao_amount = FloatPrompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of TAO to provide[/{COLORS.G.SUBHEAD_MAIN}]" + ) + + tao_balance = Balance.from_tao(tao_amount) + + # Calculate liquidity from TAO + # L = tao / (sqrt_price_high - sqrt_price_low) + sqrt_price_low = math.sqrt(price_low) + sqrt_price_high = math.sqrt(price_high) + liquidity_to_add = Balance.from_rao( + int(tao_balance.rao / (sqrt_price_high - sqrt_price_low)) + ) + + # Case 3: Current price is within range (both TAO and Alpha needed) + else: + console.print( + f"\n[green]The current price ({current_price.tao:.6f}) is within the range ({price_low:.6f} - {price_high:.6f}).[/green]" + ) + console.print("[green]Both TAO and Alpha tokens are needed for this position.[/green]\n") + + # Ask for hotkey if not provided + if hotkey_ss58 is None: + hotkey_input = Prompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the hotkey to take Alpha stake from (optional, press Enter to use default)[/{COLORS.G.SUBHEAD_MAIN}]", + default="" + ) + if hotkey_input: + hotkey_ss58 = get_hotkey_pub_ss58(wallet, hotkey_input) + else: + hotkey_ss58 = wallet.hotkey.ss58_address + + # Fetch TAO and Alpha balances + with console.status(":satellite: Fetching balances...", spinner="aesthetic"): + tao_balance_available, alpha_balance_available = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_stake_for_coldkey_and_hotkey( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid + ) + ) + + # Calculate maximum liquidity + max_liquidity, max_tao_needed, max_alpha_needed = calculate_max_liquidity_from_balances( + tao_balance=tao_balance_available, + alpha_balance=alpha_balance_available, + current_price=current_price, + price_low=price_low_balance, + price_high=price_high_balance, + ) + + console.print( + f"\n[cyan]Maximum liquidity that can be provided:[/cyan]\n" + f" TAO: {max_tao_needed.tao:.6f} τ\n" + f" Alpha: {max_alpha_needed.tao:.6f} α (for subnet {netuid})\n" + ) + + # Ask user to enter TAO or Alpha amount + choice = Prompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter 'tao' to specify TAO amount or 'alpha' to specify Alpha amount[/{COLORS.G.SUBHEAD_MAIN}]", + choices=["tao", "alpha"], + default="tao" + ) + + if choice == "tao": + if tao_amount is None: + tao_amount = FloatPrompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of TAO to provide (max: {max_tao_needed.tao:.6f})[/{COLORS.G.SUBHEAD_MAIN}]" + ) + tao_to_provide = Balance.from_tao(tao_amount) + + # Calculate corresponding Alpha + alpha_to_provide = calculate_alpha_from_tao( + tao_amount=tao_to_provide, + current_price=current_price, + price_low=price_low_balance, + price_high=price_high_balance, + ) + + console.print( + f"[cyan]This will require {alpha_to_provide.tao:.6f} Alpha tokens[/cyan]" + ) + + # Calculate liquidity + sqrt_current_price = math.sqrt(current_price.tao) + sqrt_price_low = math.sqrt(price_low) + liquidity_to_add = Balance.from_rao( + int(tao_to_provide.rao / (sqrt_current_price - sqrt_price_low)) + ) + else: + if alpha_amount is None: + alpha_amount = FloatPrompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of Alpha to provide (max: {max_alpha_needed.tao:.6f})[/{COLORS.G.SUBHEAD_MAIN}]" + ) + alpha_to_provide = Balance.from_tao(alpha_amount) + + # Calculate corresponding TAO + tao_to_provide = calculate_tao_from_alpha( + alpha_amount=alpha_to_provide, + current_price=current_price, + price_low=price_low_balance, + price_high=price_high_balance, + ) + + console.print( + f"[cyan]This will require {tao_to_provide.tao:.6f} TAO tokens[/cyan]" + ) + + # Calculate liquidity + sqrt_current_price = math.sqrt(current_price.tao) + sqrt_price_high = math.sqrt(price_high) + liquidity_to_add = Balance.from_rao( + int(alpha_to_provide.rao / (1 / sqrt_current_price - 1 / sqrt_price_high)) + ) + + # Step 6: Execute the extrinsic + return await add_liquidity( + subtensor=subtensor, + wallet=wallet, + hotkey_ss58=hotkey_ss58, + netuid=netuid, + liquidity=liquidity_to_add, + price_low=price_low_balance, + price_high=price_high_balance, + prompt=prompt, + json_output=json_output, + ) + + async def get_liquidity_list( subtensor: "SubtensorInterface", wallet: "Wallet", diff --git a/bittensor_cli/src/commands/liquidity/utils.py b/bittensor_cli/src/commands/liquidity/utils.py index f364a64e4..82f3377cf 100644 --- a/bittensor_cli/src/commands/liquidity/utils.py +++ b/bittensor_cli/src/commands/liquidity/utils.py @@ -200,3 +200,151 @@ def prompt_position_id() -> int: console.print("[red]Please enter a valid number[/red].") # will never return this, but fixes the type checker return 0 + + +def calculate_max_liquidity_from_balances( + tao_balance: Balance, + alpha_balance: Balance, + current_price: Balance, + price_low: Balance, + price_high: Balance, +) -> tuple[Balance, Balance, Balance]: + """Calculate the maximum liquidity that can be provided given TAO and Alpha balances. + + Arguments: + tao_balance: Available TAO balance + alpha_balance: Available Alpha balance + current_price: Current subnet price (Alpha/TAO) + price_low: Lower bound of the price range + price_high: Upper bound of the price range + + Returns: + tuple[Balance, Balance, Balance]: + - Maximum liquidity that can be provided + - TAO amount needed for this liquidity + - Alpha amount needed for this liquidity + """ + sqrt_price_low = math.sqrt(price_low.tao) + sqrt_price_high = math.sqrt(price_high.tao) + sqrt_current_price = math.sqrt(current_price.tao) + + # Case 1: Current price is below the range (only Alpha needed) + if sqrt_current_price < sqrt_price_low: + # L = alpha / (1/sqrt_price_low - 1/sqrt_price_high) + max_liquidity_rao = alpha_balance.rao / (1 / sqrt_price_low - 1 / sqrt_price_high) + return ( + Balance.from_rao(int(max_liquidity_rao)), + Balance.from_rao(0), # No TAO needed + alpha_balance, + ) + + # Case 2: Current price is above the range (only TAO needed) + elif sqrt_current_price > sqrt_price_high: + # L = tao / (sqrt_price_high - sqrt_price_low) + max_liquidity_rao = tao_balance.rao / (sqrt_price_high - sqrt_price_low) + return ( + Balance.from_rao(int(max_liquidity_rao)), + tao_balance, + Balance.from_rao(0), # No Alpha needed + ) + + # Case 3: Current price is within the range (both TAO and Alpha needed) + else: + # Calculate liquidity from TAO: L = tao / (sqrt_current_price - sqrt_price_low) + liquidity_from_tao = tao_balance.rao / (sqrt_current_price - sqrt_price_low) + + # Calculate liquidity from Alpha: L = alpha / (1/sqrt_current_price - 1/sqrt_price_high) + liquidity_from_alpha = alpha_balance.rao / ( + 1 / sqrt_current_price - 1 / sqrt_price_high + ) + + # Maximum liquidity is limited by the smaller of the two + max_liquidity_rao = min(liquidity_from_tao, liquidity_from_alpha) + + # Calculate the actual amounts needed + tao_needed_rao = max_liquidity_rao * (sqrt_current_price - sqrt_price_low) + alpha_needed_rao = max_liquidity_rao * ( + 1 / sqrt_current_price - 1 / sqrt_price_high + ) + + return ( + Balance.from_rao(int(max_liquidity_rao)), + Balance.from_rao(int(tao_needed_rao)), + Balance.from_rao(int(alpha_needed_rao)), + ) + + +def calculate_alpha_from_tao( + tao_amount: Balance, + current_price: Balance, + price_low: Balance, + price_high: Balance, +) -> Balance: + """Calculate the Alpha amount needed for a given TAO amount. + + Arguments: + tao_amount: TAO amount to provide + current_price: Current subnet price (Alpha/TAO) + price_low: Lower bound of the price range + price_high: Upper bound of the price range + + Returns: + Balance: Alpha amount needed + """ + sqrt_price_low = math.sqrt(price_low.tao) + sqrt_price_high = math.sqrt(price_high.tao) + sqrt_current_price = math.sqrt(current_price.tao) + + # If current price is below range, no TAO should be provided + if sqrt_current_price < sqrt_price_low: + return Balance.from_rao(0) + + # If current price is above range, no Alpha is needed + if sqrt_current_price > sqrt_price_high: + return Balance.from_rao(0) + + # Calculate liquidity from TAO + liquidity_rao = tao_amount.rao / (sqrt_current_price - sqrt_price_low) + + # Calculate Alpha needed for this liquidity + alpha_needed_rao = liquidity_rao * (1 / sqrt_current_price - 1 / sqrt_price_high) + + return Balance.from_rao(int(alpha_needed_rao)) + + +def calculate_tao_from_alpha( + alpha_amount: Balance, + current_price: Balance, + price_low: Balance, + price_high: Balance, +) -> Balance: + """Calculate the TAO amount needed for a given Alpha amount. + + Arguments: + alpha_amount: Alpha amount to provide + current_price: Current subnet price (Alpha/TAO) + price_low: Lower bound of the price range + price_high: Upper bound of the price range + + Returns: + Balance: TAO amount needed + """ + sqrt_price_low = math.sqrt(price_low.tao) + sqrt_price_high = math.sqrt(price_high.tao) + sqrt_current_price = math.sqrt(current_price.tao) + + # If current price is above range, no Alpha should be provided + if sqrt_current_price > sqrt_price_high: + return Balance.from_rao(0) + + # If current price is below range, no TAO is needed + if sqrt_current_price < sqrt_price_low: + return Balance.from_rao(0) + + # Calculate liquidity from Alpha + liquidity_rao = alpha_amount.rao / (1 / sqrt_current_price - 1 / sqrt_price_high) + + # Calculate TAO needed for this liquidity + tao_needed_rao = liquidity_rao * (sqrt_current_price - sqrt_price_low) + + return Balance.from_rao(int(tao_needed_rao)) diff --git a/tests/unit_tests/test_liquidity_utils.py b/tests/unit_tests/test_liquidity_utils.py new file mode 100644 index 000000000..fe5e3cdaf --- /dev/null +++ b/tests/unit_tests/test_liquidity_utils.py @@ -0,0 +1,166 @@ +"""Unit tests for liquidity utility functions.""" +import math +import pytest +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.commands.liquidity.utils import ( + calculate_max_liquidity_from_balances, + calculate_alpha_from_tao, + calculate_tao_from_alpha, +) + + +class TestLiquidityCalculations: + """Test the new liquidity calculation helper functions.""" + + def test_calculate_max_liquidity_only_alpha_needed(self): + """Test when current price is below the range (only Alpha needed).""" + tao_balance = Balance.from_tao(100.0) + alpha_balance = Balance.from_tao(50.0) + current_price = Balance.from_tao(1.0) # Below range + price_low = Balance.from_tao(2.0) + price_high = Balance.from_tao(3.0) + + max_liquidity, max_tao, max_alpha = calculate_max_liquidity_from_balances( + tao_balance, alpha_balance, current_price, price_low, price_high + ) + + # When price is below range, only Alpha is needed + assert max_tao.rao == 0, "No TAO should be needed when price is below range" + assert max_alpha.rao == alpha_balance.rao, "All available Alpha should be used" + assert max_liquidity.rao > 0, "Liquidity should be calculated" + + def test_calculate_max_liquidity_only_tao_needed(self): + """Test when current price is above the range (only TAO needed).""" + tao_balance = Balance.from_tao(100.0) + alpha_balance = Balance.from_tao(50.0) + current_price = Balance.from_tao(5.0) # Above range + price_low = Balance.from_tao(2.0) + price_high = Balance.from_tao(3.0) + + max_liquidity, max_tao, max_alpha = calculate_max_liquidity_from_balances( + tao_balance, alpha_balance, current_price, price_low, price_high + ) + + # When price is above range, only TAO is needed + assert max_tao.rao == tao_balance.rao, "All available TAO should be used" + assert max_alpha.rao == 0, "No Alpha should be needed when price is above range" + assert max_liquidity.rao > 0, "Liquidity should be calculated" + + def test_calculate_max_liquidity_both_needed(self): + """Test when current price is within the range (both TAO and Alpha needed).""" + tao_balance = Balance.from_tao(100.0) + alpha_balance = Balance.from_tao(50.0) + current_price = Balance.from_tao(2.5) # Within range + price_low = Balance.from_tao(2.0) + price_high = Balance.from_tao(3.0) + + max_liquidity, max_tao, max_alpha = calculate_max_liquidity_from_balances( + tao_balance, alpha_balance, current_price, price_low, price_high + ) + + # When price is within range, both are needed + assert max_tao.rao > 0, "TAO should be needed when price is within range" + assert max_alpha.rao > 0, "Alpha should be needed when price is within range" + assert max_liquidity.rao > 0, "Liquidity should be calculated" + # Should not exceed available balances + assert max_tao.rao <= tao_balance.rao, "TAO needed should not exceed balance" + assert max_alpha.rao <= alpha_balance.rao, "Alpha needed should not exceed balance" + + def test_calculate_alpha_from_tao_within_range(self): + """Test calculating Alpha amount from TAO when price is within range.""" + tao_amount = Balance.from_tao(10.0) + current_price = Balance.from_tao(2.5) + price_low = Balance.from_tao(2.0) + price_high = Balance.from_tao(3.0) + + alpha_needed = calculate_alpha_from_tao( + tao_amount, current_price, price_low, price_high + ) + + assert alpha_needed.rao > 0, "Alpha should be needed for TAO within range" + + def test_calculate_alpha_from_tao_below_range(self): + """Test that no Alpha is calculated when price is below range.""" + tao_amount = Balance.from_tao(10.0) + current_price = Balance.from_tao(1.0) # Below range + price_low = Balance.from_tao(2.0) + price_high = Balance.from_tao(3.0) + + alpha_needed = calculate_alpha_from_tao( + tao_amount, current_price, price_low, price_high + ) + + assert alpha_needed.rao == 0, "No Alpha needed when price is below range" + + def test_calculate_alpha_from_tao_above_range(self): + """Test that no Alpha is needed when price is above range.""" + tao_amount = Balance.from_tao(10.0) + current_price = Balance.from_tao(5.0) # Above range + price_low = Balance.from_tao(2.0) + price_high = Balance.from_tao(3.0) + + alpha_needed = calculate_alpha_from_tao( + tao_amount, current_price, price_low, price_high + ) + + assert alpha_needed.rao == 0, "No Alpha needed when price is above range" + + def test_calculate_tao_from_alpha_within_range(self): + """Test calculating TAO amount from Alpha when price is within range.""" + alpha_amount = Balance.from_tao(10.0) + current_price = Balance.from_tao(2.5) + price_low = Balance.from_tao(2.0) + price_high = Balance.from_tao(3.0) + + tao_needed = calculate_tao_from_alpha( + alpha_amount, current_price, price_low, price_high + ) + + assert tao_needed.rao > 0, "TAO should be needed for Alpha within range" + + def test_calculate_tao_from_alpha_below_range(self): + """Test that no TAO is needed when price is below range.""" + alpha_amount = Balance.from_tao(10.0) + current_price = Balance.from_tao(1.0) # Below range + price_low = Balance.from_tao(2.0) + price_high = Balance.from_tao(3.0) + + tao_needed = calculate_tao_from_alpha( + alpha_amount, current_price, price_low, price_high + ) + + assert tao_needed.rao == 0, "No TAO needed when price is below range" + + def test_calculate_tao_from_alpha_above_range(self): + """Test that no TAO is calculated when price is above range.""" + alpha_amount = Balance.from_tao(10.0) + current_price = Balance.from_tao(5.0) # Above range + price_low = Balance.from_tao(2.0) + price_high = Balance.from_tao(3.0) + + tao_needed = calculate_tao_from_alpha( + alpha_amount, current_price, price_low, price_high + ) + + assert tao_needed.rao == 0, "No TAO calculated when price is above range" + + def test_reciprocal_calculation(self): + """Test that TAO->Alpha->TAO conversion is consistent.""" + tao_amount = Balance.from_tao(10.0) + current_price = Balance.from_tao(2.5) + price_low = Balance.from_tao(2.0) + price_high = Balance.from_tao(3.0) + + # Calculate Alpha from TAO + alpha_needed = calculate_alpha_from_tao( + tao_amount, current_price, price_low, price_high + ) + + # Calculate TAO back from Alpha + tao_back = calculate_tao_from_alpha( + alpha_needed, current_price, price_low, price_high + ) + + # Should be approximately equal (within rounding error) + assert abs(tao_back.rao - tao_amount.rao) < 1000, \ + "Reciprocal calculation should yield similar result" From 9c48ac27a282b6e542c88c75bd830d1c2216aa49 Mon Sep 17 00:00:00 2001 From: Mobile-Crest Date: Thu, 4 Dec 2025 18:56:14 +0100 Subject: [PATCH 2/4] refactor and bugfix --- bittensor_cli/cli.py | 6 - .../src/bittensor/extrinsics/liquidity.py | 211 +++++++ .../src/commands/liquidity/liquidity.py | 540 ++++++++++-------- tests/e2e_tests/test_liquidity.py | 2 +- 4 files changed, 528 insertions(+), 231 deletions(-) create mode 100644 bittensor_cli/src/bittensor/extrinsics/liquidity.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 827afa4bd..d3f0a1ce6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7344,11 +7344,6 @@ def liquidity_add( wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, netuid: Optional[int] = Options.netuid, - liquidity_: Optional[float] = typer.Option( - None, - "--liquidity", - help="Amount of liquidity to add to the subnet.", - ), price_low: Optional[float] = typer.Option( None, "--price-low", @@ -7400,7 +7395,6 @@ def liquidity_add( wallet_path=wallet_path, wallet_hotkey=wallet_hotkey, netuid=netuid, - liquidity_=liquidity_, price_low=price_low, price_high=price_high, tao_amount=tao_amount, diff --git a/bittensor_cli/src/bittensor/extrinsics/liquidity.py b/bittensor_cli/src/bittensor/extrinsics/liquidity.py new file mode 100644 index 000000000..c83072fd2 --- /dev/null +++ b/bittensor_cli/src/bittensor/extrinsics/liquidity.py @@ -0,0 +1,211 @@ +from typing import TYPE_CHECKING, Optional + +from async_substrate_interface import AsyncExtrinsicReceipt + +from bittensor_cli.src.bittensor.utils import unlock_key +from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.commands.liquidity.utils import price_to_tick + +if TYPE_CHECKING: + from bittensor_wallet import Wallet + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def add_liquidity_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + hotkey_ss58: str, + netuid: int, + liquidity: Balance, + price_low: Balance, + price_high: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: + """ + Adds liquidity to the specified price range. + + Arguments: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + hotkey_ss58: the SS58 of the hotkey to use for this transaction. + netuid: The UID of the target subnet for which the call is being initiated. + liquidity: The amount of liquidity to be added. + price_low: The lower bound of the price tick range. + price_high: The upper bound of the price tick range. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. + wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call + `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + if not (unlock := unlock_key(wallet)).success: + return False, unlock.message, None + + tick_low = price_to_tick(price_low.tao) + tick_high = price_to_tick(price_high.tao) + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="add_liquidity", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "tick_low": tick_low, + "tick_high": tick_high, + "liquidity": liquidity.rao, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +async def modify_liquidity_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + hotkey_ss58: str, + netuid: int, + position_id: int, + liquidity_delta: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: + """Modifies liquidity in liquidity position by adding or removing liquidity from it. + + Arguments: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + hotkey_ss58: the SS58 of the hotkey to use for this transaction. + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative). + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. + wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Modifying is allowed even when user liquidity is enabled in specified subnet. + Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + if not (unlock := unlock_key(wallet)).success: + return False, unlock.message, None + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="modify_position", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "position_id": position_id, + "liquidity_delta": liquidity_delta.rao, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +async def remove_liquidity_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + hotkey_ss58: str, + netuid: int, + position_id: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: + """Remove liquidity and credit balances back to wallet's hotkey stake. + + Arguments: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + hotkey_ss58: the SS58 of the hotkey to use for this transaction. + netuid: The UID of the target subnet for which the call is being initiated. + position_id: The id of the position record in the pool. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. + wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + + Note: Adding is allowed even when user liquidity is enabled in specified subnet. + Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. + """ + if not (unlock := unlock_key(wallet)).success: + return False, unlock.message, None + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="remove_liquidity", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "position_id": position_id, + }, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + +async def toggle_user_liquidity_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + enable: bool, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: + """Allow to toggle user liquidity for specified subnet. + + Arguments: + subtensor: The Subtensor client instance used for blockchain interaction. + wallet: The wallet used to sign the extrinsic (must be unlocked). + netuid: The UID of the target subnet for which the call is being initiated. + enable: Boolean indicating whether to enable user liquidity. + wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. + wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. + + Returns: + Tuple[bool, str]: + - True and a success message if the extrinsic is successfully submitted or processed. + - False and an error message if the submission fails or the wallet cannot be unlocked. + """ + if not (unlock := unlock_key(wallet)).success: + return False, unlock.message, None + + call = await subtensor.substrate.compose_call( + call_module="Swap", + call_function="toggle_user_liquidity", + call_params={"netuid": netuid, "enable": enable}, + ) + + return await subtensor.sign_and_send_extrinsic( + call=call, + wallet=wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index f96c29e96..bb8f76e94 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -1,8 +1,8 @@ import asyncio import json +import math from typing import TYPE_CHECKING, Optional -from async_substrate_interface import AsyncExtrinsicReceipt from rich.prompt import Confirm, FloatPrompt, Prompt from rich.table import Column, Table @@ -13,8 +13,15 @@ err_console, json_console, print_extrinsic_id, + get_hotkey_pub_ss58, ) from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float +from bittensor_cli.src.bittensor.extrinsics.liquidity import ( + add_liquidity_extrinsic, + modify_liquidity_extrinsic, + remove_liquidity_extrinsic, + toggle_user_liquidity_extrinsic, +) from bittensor_cli.src.commands.liquidity.utils import ( LiquidityPosition, calculate_fees, @@ -25,257 +32,342 @@ calculate_alpha_from_tao, calculate_tao_from_alpha, ) +from bittensor_wallet import Wallet if TYPE_CHECKING: from bittensor_wallet import Wallet from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface - -async def add_liquidity_extrinsic( - subtensor: "SubtensorInterface", - wallet: "Wallet", - hotkey_ss58: str, - netuid: int, - liquidity: Balance, - price_low: Balance, - price_high: Balance, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: - """ - Adds liquidity to the specified price range. - - Arguments: - subtensor: The Subtensor client instance used for blockchain interaction. - wallet: The wallet used to sign the extrinsic (must be unlocked). - hotkey_ss58: the SS58 of the hotkey to use for this transaction. - netuid: The UID of the target subnet for which the call is being initiated. - liquidity: The amount of liquidity to be added. - price_low: The lower bound of the price tick range. - price_high: The upper bound of the price tick range. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. - wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. - - Returns: - Tuple[bool, str]: - - True and a success message if the extrinsic is successfully submitted or processed. - - False and an error message if the submission fails or the wallet cannot be unlocked. - - Note: Adding is allowed even when user liquidity is enabled in specified subnet. Call - `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. - """ - if not (unlock := unlock_key(wallet)).success: - return False, unlock.message, None - - tick_low = price_to_tick(price_low.tao) - tick_high = price_to_tick(price_high.tao) - - call = await subtensor.substrate.compose_call( - call_module="Swap", - call_function="add_liquidity", - call_params={ - "hotkey": hotkey_ss58, - "netuid": netuid, - "tick_low": tick_low, - "tick_high": tick_high, - "liquidity": liquidity.rao, - }, - ) - - return await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -async def modify_liquidity_extrinsic( - subtensor: "SubtensorInterface", - wallet: "Wallet", - hotkey_ss58: str, - netuid: int, - position_id: int, - liquidity_delta: Balance, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: - """Modifies liquidity in liquidity position by adding or removing liquidity from it. - - Arguments: - subtensor: The Subtensor client instance used for blockchain interaction. - wallet: The wallet used to sign the extrinsic (must be unlocked). - hotkey_ss58: the SS58 of the hotkey to use for this transaction. - netuid: The UID of the target subnet for which the call is being initiated. - position_id: The id of the position record in the pool. - liquidity_delta: The amount of liquidity to be added or removed (add if positive or remove if negative). - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. - wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. - - Returns: - Tuple[bool, str]: - - True and a success message if the extrinsic is successfully submitted or processed. - - False and an error message if the submission fails or the wallet cannot be unlocked. - - Note: Modifying is allowed even when user liquidity is enabled in specified subnet. - Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. - """ - if not (unlock := unlock_key(wallet)).success: - return False, unlock.message, None - - call = await subtensor.substrate.compose_call( - call_module="Swap", - call_function="modify_position", - call_params={ - "hotkey": hotkey_ss58, - "netuid": netuid, - "position_id": position_id, - "liquidity_delta": liquidity_delta.rao, - }, - ) - - return await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -async def remove_liquidity_extrinsic( - subtensor: "SubtensorInterface", - wallet: "Wallet", - hotkey_ss58: str, - netuid: int, - position_id: int, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: - """Remove liquidity and credit balances back to wallet's hotkey stake. - - Arguments: - subtensor: The Subtensor client instance used for blockchain interaction. - wallet: The wallet used to sign the extrinsic (must be unlocked). - hotkey_ss58: the SS58 of the hotkey to use for this transaction. - netuid: The UID of the target subnet for which the call is being initiated. - position_id: The id of the position record in the pool. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. - wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. - - Returns: - Tuple[bool, str]: - - True and a success message if the extrinsic is successfully submitted or processed. - - False and an error message if the submission fails or the wallet cannot be unlocked. - - Note: Adding is allowed even when user liquidity is enabled in specified subnet. - Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. - """ - if not (unlock := unlock_key(wallet)).success: - return False, unlock.message, None - - call = await subtensor.substrate.compose_call( - call_module="Swap", - call_function="remove_liquidity", - call_params={ - "hotkey": hotkey_ss58, - "netuid": netuid, - "position_id": position_id, - }, - ) - - return await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -async def toggle_user_liquidity_extrinsic( +async def add_liquidity_interactive( subtensor: "SubtensorInterface", wallet: "Wallet", netuid: int, - enable: bool, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: - """Allow to toggle user liquidity for specified subnet. - - Arguments: - subtensor: The Subtensor client instance used for blockchain interaction. - wallet: The wallet used to sign the extrinsic (must be unlocked). - netuid: The UID of the target subnet for which the call is being initiated. - enable: Boolean indicating whether to enable user liquidity. - wait_for_inclusion: Whether to wait for the extrinsic to be included in a block. Defaults to True. - wait_for_finalization: Whether to wait for finalization of the extrinsic. Defaults to False. - - Returns: - Tuple[bool, str]: - - True and a success message if the extrinsic is successfully submitted or processed. - - False and an error message if the submission fails or the wallet cannot be unlocked. - """ - if not (unlock := unlock_key(wallet)).success: - return False, unlock.message, None - - call = await subtensor.substrate.compose_call( - call_module="Swap", - call_function="toggle_user_liquidity", - call_params={"netuid": netuid, "enable": enable}, - ) - - return await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - -# Command -async def add_liquidity( - subtensor: "SubtensorInterface", - wallet: "Wallet", - hotkey_ss58: str, - netuid: Optional[int], - liquidity: Balance, - price_low: Balance, - price_high: Balance, + price_low: Optional[float], + price_high: Optional[float], + tao_amount: Optional[float], + alpha_amount: Optional[float], prompt: bool, json_output: bool, ) -> tuple[bool, str]: - """Add liquidity position to provided subnet.""" - # Check wallet access - if not (ulw := unlock_key(wallet)).success: - return False, ulw.message - - # Check that the subnet exists. + """Interactive flow for adding liquidity based on the improved logic. + + Steps: + 1. Check if subnet exists + 2. Ask user to enter low and high position prices + 3. Fetch current SN price + 4. Based on price position: + - If low >= current: only ask for Alpha amount + - If high <= current: only ask for TAO amount + - Otherwise: calculate max liquidity and ask for TAO or Alpha amount + 5. Execute the extrinsic + """ + # Step 2: Check if the subnet exists if not await subtensor.subnet_exists(netuid=netuid): return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." - + + # Check if user liquidity is enabled for this subnet + with console.status(":satellite: Checking user liquidity status...", spinner="aesthetic"): + hyperparams = await subtensor.get_subnet_hyperparameters(netuid=netuid) + + if not hyperparams: + return False, f"Failed to get hyperparameters for subnet {netuid}." + + if not hyperparams.user_liquidity_enabled: + err_console.print( + f"[red]User liquidity is disabled for subnet {netuid}.[/red]\n" + ) + return False, f"User liquidity is disabled for subnet {netuid}." + + console.print(f"[green]✓ User liquidity is enabled for subnet {netuid}[/green]\n") + + # Step 3: Ask user to enter low and high position prices + if price_low is None: + while True: + price_low_input = FloatPrompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the low price for the liquidity position[/{COLORS.G.SUBHEAD_MAIN}]" + ) + if price_low_input > 0: + price_low = price_low_input + break + console.print("[red]Price must be greater than 0[/red]") + + if price_high is None: + while True: + price_high_input = FloatPrompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the high price for the liquidity position[/{COLORS.G.SUBHEAD_MAIN}]" + ) + if price_high_input > price_low: + price_high = price_high_input + break + console.print(f"[red]High price must be greater than low price ({price_low})[/red]") + + price_low_balance = Balance.from_tao(price_low) + price_high_balance = Balance.from_tao(price_high) + + # Step 4: Fetch current SN price + with console.status(":satellite: Fetching current subnet price...", spinner="aesthetic"): + current_price = await subtensor.get_subnet_price(netuid=netuid) + + console.print(f"Current subnet price: [cyan]{current_price.tao:.6f} τ[/cyan]") + + # Determine hotkey to use - default to wallet's hotkey + hotkey_ss58 = get_hotkey_pub_ss58(wallet) + + # Step 5: Determine which case we're in based on price position + liquidity_to_add = None + tao_to_provide = Balance.from_tao(0) + alpha_to_provide = Balance.from_tao(0) + + # Case 1: Low price >= current price (only Alpha needed) + if price_low >= current_price.tao: + console.print( + f"\n[yellow]The low price ({price_low:.6f}) is higher than or equal to the current price ({current_price.tao:.6f}).[/yellow]" + ) + console.print("[yellow]Only Alpha tokens are needed for this position.[/yellow]\n") + + # Fetch Alpha balance + with console.status(":satellite: Fetching Alpha balance...", spinner="aesthetic"): + alpha_balance_available = await subtensor.get_stake_for_coldkey_and_hotkey( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid + ) + + console.print(f"Available Alpha: {alpha_balance_available.tao:.6f} α (for subnet {netuid})\n") + + # Ask for Alpha amount + if alpha_amount is None: + alpha_amount = FloatPrompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of Alpha to provide[/{COLORS.G.SUBHEAD_MAIN}]" + ) + + alpha_to_provide = Balance.from_tao(alpha_amount) + + # Check if user has enough Alpha + if alpha_to_provide > alpha_balance_available: + err_console.print( + f"[red]Insufficient Alpha balance.[/red]\n" + f"Required: {alpha_to_provide.tao:.6f} α (for subnet {netuid})\n" + f"Available: {alpha_balance_available.tao:.6f} α (for subnet {netuid})" + ) + return False, "Insufficient Alpha balance." + + # Calculate liquidity from Alpha + # L = alpha / (1/sqrt_price_low - 1/sqrt_price_high) + sqrt_price_low = math.sqrt(price_low) + sqrt_price_high = math.sqrt(price_high) + liquidity_to_add = Balance.from_rao( + int(alpha_to_provide.rao / (1 / sqrt_price_low - 1 / sqrt_price_high)) + ) + + # Case 2: High price <= current price (only TAO needed) + elif price_high <= current_price.tao: + console.print( + f"\n[yellow]The high price ({price_high:.6f}) is lower than or equal to the current price ({current_price.tao:.6f}).[/yellow]" + ) + console.print("[yellow]Only TAO tokens are needed for this position.[/yellow]\n") + + # Fetch TAO balance + with console.status(":satellite: Fetching TAO balance...", spinner="aesthetic"): + tao_balance_available = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + + console.print(f"Available TAO: {tao_balance_available.tao:.6f} τ\n") + + # Ask for TAO amount + if tao_amount is None: + tao_amount = FloatPrompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of TAO to provide[/{COLORS.G.SUBHEAD_MAIN}]" + ) + + tao_to_provide = Balance.from_tao(tao_amount) + + # Check if user has enough TAO + if tao_to_provide > tao_balance_available: + err_console.print( + f"[red]Insufficient TAO balance.[/red]\n" + f"Required: {tao_to_provide.tao:.6f} τ\n" + f"Available: {tao_balance_available.tao:.6f} τ" + ) + return False, "Insufficient TAO balance." + + # Calculate liquidity from TAO + # L = tao / (sqrt_price_high - sqrt_price_low) + sqrt_price_low = math.sqrt(price_low) + sqrt_price_high = math.sqrt(price_high) + liquidity_to_add = Balance.from_rao( + int(tao_to_provide.rao / (sqrt_price_high - sqrt_price_low)) + ) + + # Case 3: Current price is within range (both TAO and Alpha needed) + else: + console.print( + f"\n[green]The current price ({current_price.tao:.6f}) is within the range ({price_low:.6f} - {price_high:.6f}).[/green]" + ) + console.print("[green]Both TAO and Alpha tokens are needed for this position.[/green]\n") + + # Fetch TAO and Alpha balances + with console.status(":satellite: Fetching balances...", spinner="aesthetic"): + tao_balance_available, alpha_balance_available = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_stake_for_coldkey_and_hotkey( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid + ) + ) + + # Calculate maximum liquidity + max_liquidity, max_tao_needed, max_alpha_needed = calculate_max_liquidity_from_balances( + tao_balance=tao_balance_available, + alpha_balance=alpha_balance_available, + current_price=current_price, + price_low=price_low_balance, + price_high=price_high_balance, + ) + + console.print( + f"\n[cyan]Maximum liquidity that can be provided:[/cyan]\n" + f" TAO: {max_tao_needed.tao:.6f} τ\n" + f" Alpha: {max_alpha_needed.tao:.6f} α (for subnet {netuid})\n" + ) + + # Determine which amount to use based on what was provided + if tao_amount is not None and alpha_amount is not None: + # Both provided - use TAO amount and calculate Alpha + choice = "tao" + elif tao_amount is not None: + # Only TAO provided + choice = "tao" + elif alpha_amount is not None: + # Only Alpha provided + choice = "alpha" + else: + # Neither provided - ask user + choice = Prompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter 'tao' to specify TAO amount or 'alpha' to specify Alpha amount[/{COLORS.G.SUBHEAD_MAIN}]", + choices=["tao", "alpha"], + default="tao" + ) + + if choice == "tao": + if tao_amount is None: + tao_amount = FloatPrompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of TAO to provide (max: {max_tao_needed.tao:.6f})[/{COLORS.G.SUBHEAD_MAIN}]" + ) + tao_to_provide = Balance.from_tao(tao_amount) + + # Calculate corresponding Alpha + alpha_to_provide = calculate_alpha_from_tao( + tao_amount=tao_to_provide, + current_price=current_price, + price_low=price_low_balance, + price_high=price_high_balance, + ) + + console.print( + f"[cyan]This will require {alpha_to_provide.tao:.6f} Alpha tokens[/cyan]" + ) + + # Check if user has enough balance + if tao_to_provide > tao_balance_available: + err_console.print( + f"[red]Insufficient TAO balance.[/red]\n" + f"Required: {tao_to_provide.tao:.6f} τ\n" + f"Available: {tao_balance_available.tao:.6f} τ" + ) + return False, "Insufficient TAO balance." + + if alpha_to_provide > alpha_balance_available: + err_console.print( + f"[red]Insufficient Alpha balance.[/red]\n" + f"Required: {alpha_to_provide.tao:.6f} α (for subnet {netuid})\n" + f"Available: {alpha_balance_available.tao:.6f} α (for subnet {netuid})" + ) + return False, "Insufficient Alpha balance." + + # Calculate liquidity + sqrt_current_price = math.sqrt(current_price.tao) + sqrt_price_low = math.sqrt(price_low) + liquidity_to_add = Balance.from_rao( + int(tao_to_provide.rao / (sqrt_current_price - sqrt_price_low)) + ) + else: + if alpha_amount is None: + alpha_amount = FloatPrompt.ask( + f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of Alpha to provide (max: {max_alpha_needed.tao:.6f})[/{COLORS.G.SUBHEAD_MAIN}]" + ) + alpha_to_provide = Balance.from_tao(alpha_amount) + + # Calculate corresponding TAO + tao_to_provide = calculate_tao_from_alpha( + alpha_amount=alpha_to_provide, + current_price=current_price, + price_low=price_low_balance, + price_high=price_high_balance, + ) + + console.print( + f"[cyan]This will require {tao_to_provide.tao:.6f} TAO tokens[/cyan]" + ) + + # Check if user has enough balance + if tao_to_provide > tao_balance_available: + err_console.print( + f"[red]Insufficient TAO balance.[/red]\n" + f"Required: {tao_to_provide.tao:.6f} τ\n" + f"Available: {tao_balance_available.tao:.6f} τ" + ) + return False, "Insufficient TAO balance." + + if alpha_to_provide > alpha_balance_available: + err_console.print( + f"[red]Insufficient Alpha balance.[/red]\n" + f"Required: {alpha_to_provide.tao:.6f} α (for subnet {netuid})\n" + f"Available: {alpha_balance_available.tao:.6f} α (for subnet {netuid})" + ) + return False, "Insufficient Alpha balance." + + # Calculate liquidity + sqrt_current_price = math.sqrt(current_price.tao) + sqrt_price_high = math.sqrt(price_high) + liquidity_to_add = Balance.from_rao( + int(alpha_to_provide.rao / (1 / sqrt_current_price - 1 / sqrt_price_high)) + ) + + # Step 6: Confirm and execute the extrinsic if prompt: console.print( "You are about to add a LiquidityPosition with:\n" - f"\tliquidity: {liquidity}\n" - f"\tprice low: {price_low}\n" - f"\tprice high: {price_high}\n" + f"\tTAO amount: {tao_to_provide.tao:.6f} τ\n" + f"\tAlpha amount: {alpha_to_provide.tao:.6f} α (for subnet {netuid})\n" + f"\tprice low: {price_low_balance}\n" + f"\tprice high: {price_high_balance}\n" f"\tto SN: {netuid}\n" f"\tusing wallet with name: {wallet.name}" ) if not Confirm.ask("Would you like to continue?"): return False, "User cancelled operation." + + # Unlock wallet before executing extrinsic + if not (ulw := unlock_key(wallet)).success: + return False, ulw.message success, message, ext_receipt = await add_liquidity_extrinsic( subtensor=subtensor, wallet=wallet, hotkey_ss58=hotkey_ss58, netuid=netuid, - liquidity=liquidity, - price_low=price_low, - price_high=price_high, + liquidity=liquidity_to_add, + price_low=price_low_balance, + price_high=price_high_balance, ) - await print_extrinsic_id(ext_receipt) - ext_id = await ext_receipt.get_extrinsic_identifier() + + ext_id = None + if ext_receipt: + await print_extrinsic_id(ext_receipt) + ext_id = await ext_receipt.get_extrinsic_identifier() + if json_output: json_console.print( json.dumps( @@ -289,6 +381,7 @@ async def add_liquidity( ) else: err_console.print(f"[red]Error: {message}[/red]") + return success, message @@ -298,7 +391,6 @@ async def add_liquidity_interactive( wallet_path: str, wallet_hotkey: str, netuid: int, - liquidity_: Optional[float], price_low: Optional[float], price_high: Optional[float], tao_amount: Optional[float], diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 7a210f0a1..9fd159591 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -204,7 +204,7 @@ def test_liquidity(local_chain, wallet_setup): wallet_alice.hotkey_str, "--netuid", netuid, - "--liquidity", + "--tao-amount", "1.0", "--price-low", "1.7", From d897591ac84835fa1dc2877bbc9f392d5c2506a1 Mon Sep 17 00:00:00 2001 From: Mobile-Crest Date: Thu, 4 Dec 2025 19:51:25 +0100 Subject: [PATCH 3/4] fix error --- bittensor_cli/cli.py | 12 +- .../src/commands/liquidity/liquidity.py | 263 ------------------ 2 files changed, 9 insertions(+), 266 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d3f0a1ce6..9e9769ca4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7388,12 +7388,18 @@ def liquidity_add( show_default=False, ) + wallet = self.wallet_ask( + wallet_name=wallet_name, + wallet_path=wallet_path, + wallet_hotkey=wallet_hotkey, + ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], + validate=WV.WALLET, + ) + return self._run_command( liquidity.add_liquidity_interactive( subtensor=self.initialize_chain(network), - wallet_name=wallet_name, - wallet_path=wallet_path, - wallet_hotkey=wallet_hotkey, + wallet=wallet, netuid=netuid, price_low=price_low, price_high=price_high, diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index bb8f76e94..42fbdeee3 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -20,7 +20,6 @@ add_liquidity_extrinsic, modify_liquidity_extrinsic, remove_liquidity_extrinsic, - toggle_user_liquidity_extrinsic, ) from bittensor_cli.src.commands.liquidity.utils import ( LiquidityPosition, @@ -385,268 +384,6 @@ async def add_liquidity_interactive( return success, message -async def add_liquidity_interactive( - subtensor: "SubtensorInterface", - wallet_name: str, - wallet_path: str, - wallet_hotkey: str, - netuid: int, - price_low: Optional[float], - price_high: Optional[float], - tao_amount: Optional[float], - alpha_amount: Optional[float], - prompt: bool, - json_output: bool, -) -> tuple[bool, str]: - """Interactive flow for adding liquidity based on the improved logic. - - Steps: - 1. Check if subnet exists - 2. Ask user to enter low and high position prices - 3. Fetch current SN price - 4. Based on price position: - - If low >= current: only ask for Alpha amount - - If high <= current: only ask for TAO amount - - Otherwise: calculate max liquidity and ask for TAO or Alpha amount - 5. Execute the extrinsic - """ - from bittensor_wallet import Wallet - from bittensor_cli.src.bittensor.utils import get_hotkey_pub_ss58 - import math - - # Load wallet - try: - wallet = Wallet(name=wallet_name, path=wallet_path, hotkey=wallet_hotkey) - except Exception as e: - return False, f"Failed to load wallet: {e}" - - # Step 2: Check if the subnet exists - if not await subtensor.subnet_exists(netuid=netuid): - return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." - - # Step 3: Ask user to enter low and high position prices - if price_low is None: - while True: - price_low_input = FloatPrompt.ask( - f"[{COLORS.G.SUBHEAD_MAIN}]Enter the low price for the liquidity position[/{COLORS.G.SUBHEAD_MAIN}]" - ) - if price_low_input > 0: - price_low = price_low_input - break - console.print("[red]Price must be greater than 0[/red]") - - if price_high is None: - while True: - price_high_input = FloatPrompt.ask( - f"[{COLORS.G.SUBHEAD_MAIN}]Enter the high price for the liquidity position[/{COLORS.G.SUBHEAD_MAIN}]" - ) - if price_high_input > price_low: - price_high = price_high_input - break - console.print(f"[red]High price must be greater than low price ({price_low})[/red]") - - price_low_balance = Balance.from_tao(price_low) - price_high_balance = Balance.from_tao(price_high) - - # Step 4: Fetch current SN price - with console.status(":satellite: Fetching current subnet price...", spinner="aesthetic"): - current_price = await subtensor.get_subnet_price(netuid=netuid) - - console.print(f"Current subnet price: [cyan]{current_price.tao:.6f} τ[/cyan]") - - # Determine hotkey to use (optional for some cases) - hotkey_ss58 = None - if wallet_hotkey: - hotkey_ss58 = get_hotkey_pub_ss58(wallet, wallet_hotkey) - - # Step 5: Determine which case we're in based on price position - liquidity_to_add = None - - # Case 1: Low price >= current price (only Alpha needed) - if price_low >= current_price.tao: - console.print( - f"\n[yellow]The low price ({price_low:.6f}) is higher than or equal to the current price ({current_price.tao:.6f}).[/yellow]" - ) - console.print("[yellow]Only Alpha tokens are needed for this position.[/yellow]\n") - - # Ask for hotkey if not provided - if hotkey_ss58 is None: - hotkey_input = Prompt.ask( - f"[{COLORS.G.SUBHEAD_MAIN}]Enter the hotkey to take Alpha stake from (optional, press Enter to skip)[/{COLORS.G.SUBHEAD_MAIN}]", - default="" - ) - if hotkey_input: - hotkey_ss58 = get_hotkey_pub_ss58(wallet, hotkey_input) - else: - # Use default hotkey from wallet - hotkey_ss58 = wallet.hotkey.ss58_address - - # Ask for Alpha amount - if alpha_amount is None: - alpha_amount = FloatPrompt.ask( - f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of Alpha to provide[/{COLORS.G.SUBHEAD_MAIN}]" - ) - - alpha_balance = Balance.from_tao(alpha_amount) - - # Calculate liquidity from Alpha - # L = alpha / (1/sqrt_price_low - 1/sqrt_price_high) - sqrt_price_low = math.sqrt(price_low) - sqrt_price_high = math.sqrt(price_high) - liquidity_to_add = Balance.from_rao( - int(alpha_balance.rao / (1 / sqrt_price_low - 1 / sqrt_price_high)) - ) - - # Case 2: High price <= current price (only TAO needed) - elif price_high <= current_price.tao: - console.print( - f"\n[yellow]The high price ({price_high:.6f}) is lower than or equal to the current price ({current_price.tao:.6f}).[/yellow]" - ) - console.print("[yellow]Only TAO tokens are needed for this position.[/yellow]\n") - - # Ask for hotkey if not provided (has no effect but required for extrinsic) - if hotkey_ss58 is None: - hotkey_input = Prompt.ask( - f"[{COLORS.G.SUBHEAD_MAIN}]Enter the hotkey (optional, press Enter to use default)[/{COLORS.G.SUBHEAD_MAIN}]", - default="" - ) - if hotkey_input: - hotkey_ss58 = get_hotkey_pub_ss58(wallet, hotkey_input) - else: - hotkey_ss58 = wallet.hotkey.ss58_address - - # Ask for TAO amount - if tao_amount is None: - tao_amount = FloatPrompt.ask( - f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of TAO to provide[/{COLORS.G.SUBHEAD_MAIN}]" - ) - - tao_balance = Balance.from_tao(tao_amount) - - # Calculate liquidity from TAO - # L = tao / (sqrt_price_high - sqrt_price_low) - sqrt_price_low = math.sqrt(price_low) - sqrt_price_high = math.sqrt(price_high) - liquidity_to_add = Balance.from_rao( - int(tao_balance.rao / (sqrt_price_high - sqrt_price_low)) - ) - - # Case 3: Current price is within range (both TAO and Alpha needed) - else: - console.print( - f"\n[green]The current price ({current_price.tao:.6f}) is within the range ({price_low:.6f} - {price_high:.6f}).[/green]" - ) - console.print("[green]Both TAO and Alpha tokens are needed for this position.[/green]\n") - - # Ask for hotkey if not provided - if hotkey_ss58 is None: - hotkey_input = Prompt.ask( - f"[{COLORS.G.SUBHEAD_MAIN}]Enter the hotkey to take Alpha stake from (optional, press Enter to use default)[/{COLORS.G.SUBHEAD_MAIN}]", - default="" - ) - if hotkey_input: - hotkey_ss58 = get_hotkey_pub_ss58(wallet, hotkey_input) - else: - hotkey_ss58 = wallet.hotkey.ss58_address - - # Fetch TAO and Alpha balances - with console.status(":satellite: Fetching balances...", spinner="aesthetic"): - tao_balance_available, alpha_balance_available = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address), - subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=netuid - ) - ) - - # Calculate maximum liquidity - max_liquidity, max_tao_needed, max_alpha_needed = calculate_max_liquidity_from_balances( - tao_balance=tao_balance_available, - alpha_balance=alpha_balance_available, - current_price=current_price, - price_low=price_low_balance, - price_high=price_high_balance, - ) - - console.print( - f"\n[cyan]Maximum liquidity that can be provided:[/cyan]\n" - f" TAO: {max_tao_needed.tao:.6f} τ\n" - f" Alpha: {max_alpha_needed.tao:.6f} α (for subnet {netuid})\n" - ) - - # Ask user to enter TAO or Alpha amount - choice = Prompt.ask( - f"[{COLORS.G.SUBHEAD_MAIN}]Enter 'tao' to specify TAO amount or 'alpha' to specify Alpha amount[/{COLORS.G.SUBHEAD_MAIN}]", - choices=["tao", "alpha"], - default="tao" - ) - - if choice == "tao": - if tao_amount is None: - tao_amount = FloatPrompt.ask( - f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of TAO to provide (max: {max_tao_needed.tao:.6f})[/{COLORS.G.SUBHEAD_MAIN}]" - ) - tao_to_provide = Balance.from_tao(tao_amount) - - # Calculate corresponding Alpha - alpha_to_provide = calculate_alpha_from_tao( - tao_amount=tao_to_provide, - current_price=current_price, - price_low=price_low_balance, - price_high=price_high_balance, - ) - - console.print( - f"[cyan]This will require {alpha_to_provide.tao:.6f} Alpha tokens[/cyan]" - ) - - # Calculate liquidity - sqrt_current_price = math.sqrt(current_price.tao) - sqrt_price_low = math.sqrt(price_low) - liquidity_to_add = Balance.from_rao( - int(tao_to_provide.rao / (sqrt_current_price - sqrt_price_low)) - ) - else: - if alpha_amount is None: - alpha_amount = FloatPrompt.ask( - f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of Alpha to provide (max: {max_alpha_needed.tao:.6f})[/{COLORS.G.SUBHEAD_MAIN}]" - ) - alpha_to_provide = Balance.from_tao(alpha_amount) - - # Calculate corresponding TAO - tao_to_provide = calculate_tao_from_alpha( - alpha_amount=alpha_to_provide, - current_price=current_price, - price_low=price_low_balance, - price_high=price_high_balance, - ) - - console.print( - f"[cyan]This will require {tao_to_provide.tao:.6f} TAO tokens[/cyan]" - ) - - # Calculate liquidity - sqrt_current_price = math.sqrt(current_price.tao) - sqrt_price_high = math.sqrt(price_high) - liquidity_to_add = Balance.from_rao( - int(alpha_to_provide.rao / (1 / sqrt_current_price - 1 / sqrt_price_high)) - ) - - # Step 6: Execute the extrinsic - return await add_liquidity( - subtensor=subtensor, - wallet=wallet, - hotkey_ss58=hotkey_ss58, - netuid=netuid, - liquidity=liquidity_to_add, - price_low=price_low_balance, - price_high=price_high_balance, - prompt=prompt, - json_output=json_output, - ) - - async def get_liquidity_list( subtensor: "SubtensorInterface", wallet: "Wallet", From 194b3a24550d2c38695b706c34b81e9cc37d1f6e Mon Sep 17 00:00:00 2001 From: Mobile-Crest Date: Fri, 5 Dec 2025 09:57:52 +0100 Subject: [PATCH 4/4] run ruff format --- bittensor_cli/cli.py | 2 +- .../src/commands/liquidity/liquidity.py | 158 ++++++++++-------- bittensor_cli/src/commands/liquidity/utils.py | 50 +++--- tests/unit_tests/test_liquidity_utils.py | 8 +- 4 files changed, 124 insertions(+), 94 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9e9769ca4..7d94ab34b 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7379,7 +7379,7 @@ def liquidity_add( ): """Add liquidity to the swap (as a combination of TAO + Alpha).""" self.verbosity_handler(quiet, verbose, json_output) - + # Step 1: Ask for netuid if not netuid: netuid = Prompt.ask( diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index 42fbdeee3..2f618e16c 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -37,6 +37,7 @@ from bittensor_wallet import Wallet from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + async def add_liquidity_interactive( subtensor: "SubtensorInterface", wallet: "Wallet", @@ -49,7 +50,7 @@ async def add_liquidity_interactive( json_output: bool, ) -> tuple[bool, str]: """Interactive flow for adding liquidity based on the improved logic. - + Steps: 1. Check if subnet exists 2. Ask user to enter low and high position prices @@ -63,22 +64,24 @@ async def add_liquidity_interactive( # Step 2: Check if the subnet exists if not await subtensor.subnet_exists(netuid=netuid): return False, f"Subnet with netuid: {netuid} does not exist in {subtensor}." - + # Check if user liquidity is enabled for this subnet - with console.status(":satellite: Checking user liquidity status...", spinner="aesthetic"): + with console.status( + ":satellite: Checking user liquidity status...", spinner="aesthetic" + ): hyperparams = await subtensor.get_subnet_hyperparameters(netuid=netuid) - + if not hyperparams: return False, f"Failed to get hyperparameters for subnet {netuid}." - + if not hyperparams.user_liquidity_enabled: err_console.print( f"[red]User liquidity is disabled for subnet {netuid}.[/red]\n" ) return False, f"User liquidity is disabled for subnet {netuid}." - + console.print(f"[green]✓ User liquidity is enabled for subnet {netuid}[/green]\n") - + # Step 3: Ask user to enter low and high position prices if price_low is None: while True: @@ -89,7 +92,7 @@ async def add_liquidity_interactive( price_low = price_low_input break console.print("[red]Price must be greater than 0[/red]") - + if price_high is None: while True: price_high_input = FloatPrompt.ask( @@ -98,50 +101,60 @@ async def add_liquidity_interactive( if price_high_input > price_low: price_high = price_high_input break - console.print(f"[red]High price must be greater than low price ({price_low})[/red]") - + console.print( + f"[red]High price must be greater than low price ({price_low})[/red]" + ) + price_low_balance = Balance.from_tao(price_low) price_high_balance = Balance.from_tao(price_high) - + # Step 4: Fetch current SN price - with console.status(":satellite: Fetching current subnet price...", spinner="aesthetic"): + with console.status( + ":satellite: Fetching current subnet price...", spinner="aesthetic" + ): current_price = await subtensor.get_subnet_price(netuid=netuid) - + console.print(f"Current subnet price: [cyan]{current_price.tao:.6f} τ[/cyan]") - + # Determine hotkey to use - default to wallet's hotkey hotkey_ss58 = get_hotkey_pub_ss58(wallet) - + # Step 5: Determine which case we're in based on price position liquidity_to_add = None tao_to_provide = Balance.from_tao(0) alpha_to_provide = Balance.from_tao(0) - + # Case 1: Low price >= current price (only Alpha needed) if price_low >= current_price.tao: console.print( f"\n[yellow]The low price ({price_low:.6f}) is higher than or equal to the current price ({current_price.tao:.6f}).[/yellow]" ) - console.print("[yellow]Only Alpha tokens are needed for this position.[/yellow]\n") - + console.print( + "[yellow]Only Alpha tokens are needed for this position.[/yellow]\n" + ) + # Fetch Alpha balance - with console.status(":satellite: Fetching Alpha balance...", spinner="aesthetic"): + with console.status( + ":satellite: Fetching Alpha balance...", spinner="aesthetic" + ): alpha_balance_available = await subtensor.get_stake_for_coldkey_and_hotkey( hotkey_ss58=hotkey_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=netuid + netuid=netuid, ) - - console.print(f"Available Alpha: {alpha_balance_available.tao:.6f} α (for subnet {netuid})\n") - + + console.print( + f"Available Alpha: {alpha_balance_available.tao:.6f} α (for subnet {netuid})\n" + ) + # Ask for Alpha amount if alpha_amount is None: alpha_amount = FloatPrompt.ask( f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of Alpha to provide[/{COLORS.G.SUBHEAD_MAIN}]" ) - + alpha_to_provide = Balance.from_tao(alpha_amount) - + # Check if user has enough Alpha if alpha_to_provide > alpha_balance_available: err_console.print( @@ -150,7 +163,7 @@ async def add_liquidity_interactive( f"Available: {alpha_balance_available.tao:.6f} α (for subnet {netuid})" ) return False, "Insufficient Alpha balance." - + # Calculate liquidity from Alpha # L = alpha / (1/sqrt_price_low - 1/sqrt_price_high) sqrt_price_low = math.sqrt(price_low) @@ -158,28 +171,32 @@ async def add_liquidity_interactive( liquidity_to_add = Balance.from_rao( int(alpha_to_provide.rao / (1 / sqrt_price_low - 1 / sqrt_price_high)) ) - + # Case 2: High price <= current price (only TAO needed) elif price_high <= current_price.tao: console.print( f"\n[yellow]The high price ({price_high:.6f}) is lower than or equal to the current price ({current_price.tao:.6f}).[/yellow]" ) - console.print("[yellow]Only TAO tokens are needed for this position.[/yellow]\n") - + console.print( + "[yellow]Only TAO tokens are needed for this position.[/yellow]\n" + ) + # Fetch TAO balance with console.status(":satellite: Fetching TAO balance...", spinner="aesthetic"): - tao_balance_available = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - + tao_balance_available = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + console.print(f"Available TAO: {tao_balance_available.tao:.6f} τ\n") - + # Ask for TAO amount if tao_amount is None: tao_amount = FloatPrompt.ask( f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of TAO to provide[/{COLORS.G.SUBHEAD_MAIN}]" ) - + tao_to_provide = Balance.from_tao(tao_amount) - + # Check if user has enough TAO if tao_to_provide > tao_balance_available: err_console.print( @@ -188,7 +205,7 @@ async def add_liquidity_interactive( f"Available: {tao_balance_available.tao:.6f} τ" ) return False, "Insufficient TAO balance." - + # Calculate liquidity from TAO # L = tao / (sqrt_price_high - sqrt_price_low) sqrt_price_low = math.sqrt(price_low) @@ -196,14 +213,16 @@ async def add_liquidity_interactive( liquidity_to_add = Balance.from_rao( int(tao_to_provide.rao / (sqrt_price_high - sqrt_price_low)) ) - + # Case 3: Current price is within range (both TAO and Alpha needed) else: console.print( f"\n[green]The current price ({current_price.tao:.6f}) is within the range ({price_low:.6f} - {price_high:.6f}).[/green]" ) - console.print("[green]Both TAO and Alpha tokens are needed for this position.[/green]\n") - + console.print( + "[green]Both TAO and Alpha tokens are needed for this position.[/green]\n" + ) + # Fetch TAO and Alpha balances with console.status(":satellite: Fetching balances...", spinner="aesthetic"): tao_balance_available, alpha_balance_available = await asyncio.gather( @@ -211,25 +230,27 @@ async def add_liquidity_interactive( subtensor.get_stake_for_coldkey_and_hotkey( hotkey_ss58=hotkey_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=netuid - ) + netuid=netuid, + ), ) - + # Calculate maximum liquidity - max_liquidity, max_tao_needed, max_alpha_needed = calculate_max_liquidity_from_balances( - tao_balance=tao_balance_available, - alpha_balance=alpha_balance_available, - current_price=current_price, - price_low=price_low_balance, - price_high=price_high_balance, + max_liquidity, max_tao_needed, max_alpha_needed = ( + calculate_max_liquidity_from_balances( + tao_balance=tao_balance_available, + alpha_balance=alpha_balance_available, + current_price=current_price, + price_low=price_low_balance, + price_high=price_high_balance, + ) ) - + console.print( f"\n[cyan]Maximum liquidity that can be provided:[/cyan]\n" f" TAO: {max_tao_needed.tao:.6f} τ\n" f" Alpha: {max_alpha_needed.tao:.6f} α (for subnet {netuid})\n" ) - + # Determine which amount to use based on what was provided if tao_amount is not None and alpha_amount is not None: # Both provided - use TAO amount and calculate Alpha @@ -245,16 +266,16 @@ async def add_liquidity_interactive( choice = Prompt.ask( f"[{COLORS.G.SUBHEAD_MAIN}]Enter 'tao' to specify TAO amount or 'alpha' to specify Alpha amount[/{COLORS.G.SUBHEAD_MAIN}]", choices=["tao", "alpha"], - default="tao" + default="tao", ) - + if choice == "tao": if tao_amount is None: tao_amount = FloatPrompt.ask( f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of TAO to provide (max: {max_tao_needed.tao:.6f})[/{COLORS.G.SUBHEAD_MAIN}]" ) tao_to_provide = Balance.from_tao(tao_amount) - + # Calculate corresponding Alpha alpha_to_provide = calculate_alpha_from_tao( tao_amount=tao_to_provide, @@ -262,11 +283,11 @@ async def add_liquidity_interactive( price_low=price_low_balance, price_high=price_high_balance, ) - + console.print( f"[cyan]This will require {alpha_to_provide.tao:.6f} Alpha tokens[/cyan]" ) - + # Check if user has enough balance if tao_to_provide > tao_balance_available: err_console.print( @@ -275,7 +296,7 @@ async def add_liquidity_interactive( f"Available: {tao_balance_available.tao:.6f} τ" ) return False, "Insufficient TAO balance." - + if alpha_to_provide > alpha_balance_available: err_console.print( f"[red]Insufficient Alpha balance.[/red]\n" @@ -283,7 +304,7 @@ async def add_liquidity_interactive( f"Available: {alpha_balance_available.tao:.6f} α (for subnet {netuid})" ) return False, "Insufficient Alpha balance." - + # Calculate liquidity sqrt_current_price = math.sqrt(current_price.tao) sqrt_price_low = math.sqrt(price_low) @@ -296,7 +317,7 @@ async def add_liquidity_interactive( f"[{COLORS.G.SUBHEAD_MAIN}]Enter the amount of Alpha to provide (max: {max_alpha_needed.tao:.6f})[/{COLORS.G.SUBHEAD_MAIN}]" ) alpha_to_provide = Balance.from_tao(alpha_amount) - + # Calculate corresponding TAO tao_to_provide = calculate_tao_from_alpha( alpha_amount=alpha_to_provide, @@ -304,11 +325,11 @@ async def add_liquidity_interactive( price_low=price_low_balance, price_high=price_high_balance, ) - + console.print( f"[cyan]This will require {tao_to_provide.tao:.6f} TAO tokens[/cyan]" ) - + # Check if user has enough balance if tao_to_provide > tao_balance_available: err_console.print( @@ -317,7 +338,7 @@ async def add_liquidity_interactive( f"Available: {tao_balance_available.tao:.6f} τ" ) return False, "Insufficient TAO balance." - + if alpha_to_provide > alpha_balance_available: err_console.print( f"[red]Insufficient Alpha balance.[/red]\n" @@ -325,14 +346,17 @@ async def add_liquidity_interactive( f"Available: {alpha_balance_available.tao:.6f} α (for subnet {netuid})" ) return False, "Insufficient Alpha balance." - + # Calculate liquidity sqrt_current_price = math.sqrt(current_price.tao) sqrt_price_high = math.sqrt(price_high) liquidity_to_add = Balance.from_rao( - int(alpha_to_provide.rao / (1 / sqrt_current_price - 1 / sqrt_price_high)) + int( + alpha_to_provide.rao + / (1 / sqrt_current_price - 1 / sqrt_price_high) + ) ) - + # Step 6: Confirm and execute the extrinsic if prompt: console.print( @@ -347,7 +371,7 @@ async def add_liquidity_interactive( if not Confirm.ask("Would you like to continue?"): return False, "User cancelled operation." - + # Unlock wallet before executing extrinsic if not (ulw := unlock_key(wallet)).success: return False, ulw.message @@ -361,12 +385,12 @@ async def add_liquidity_interactive( price_low=price_low_balance, price_high=price_high_balance, ) - + ext_id = None if ext_receipt: await print_extrinsic_id(ext_receipt) ext_id = await ext_receipt.get_extrinsic_identifier() - + if json_output: json_console.print( json.dumps( @@ -380,7 +404,7 @@ async def add_liquidity_interactive( ) else: err_console.print(f"[red]Error: {message}[/red]") - + return success, message diff --git a/bittensor_cli/src/commands/liquidity/utils.py b/bittensor_cli/src/commands/liquidity/utils.py index 82f3377cf..66680f3c9 100644 --- a/bittensor_cli/src/commands/liquidity/utils.py +++ b/bittensor_cli/src/commands/liquidity/utils.py @@ -210,14 +210,14 @@ def calculate_max_liquidity_from_balances( price_high: Balance, ) -> tuple[Balance, Balance, Balance]: """Calculate the maximum liquidity that can be provided given TAO and Alpha balances. - + Arguments: tao_balance: Available TAO balance alpha_balance: Available Alpha balance current_price: Current subnet price (Alpha/TAO) price_low: Lower bound of the price range price_high: Upper bound of the price range - + Returns: tuple[Balance, Balance, Balance]: - Maximum liquidity that can be provided @@ -227,17 +227,19 @@ def calculate_max_liquidity_from_balances( sqrt_price_low = math.sqrt(price_low.tao) sqrt_price_high = math.sqrt(price_high.tao) sqrt_current_price = math.sqrt(current_price.tao) - + # Case 1: Current price is below the range (only Alpha needed) if sqrt_current_price < sqrt_price_low: # L = alpha / (1/sqrt_price_low - 1/sqrt_price_high) - max_liquidity_rao = alpha_balance.rao / (1 / sqrt_price_low - 1 / sqrt_price_high) + max_liquidity_rao = alpha_balance.rao / ( + 1 / sqrt_price_low - 1 / sqrt_price_high + ) return ( Balance.from_rao(int(max_liquidity_rao)), Balance.from_rao(0), # No TAO needed alpha_balance, ) - + # Case 2: Current price is above the range (only TAO needed) elif sqrt_current_price > sqrt_price_high: # L = tao / (sqrt_price_high - sqrt_price_low) @@ -247,26 +249,26 @@ def calculate_max_liquidity_from_balances( tao_balance, Balance.from_rao(0), # No Alpha needed ) - + # Case 3: Current price is within the range (both TAO and Alpha needed) else: # Calculate liquidity from TAO: L = tao / (sqrt_current_price - sqrt_price_low) liquidity_from_tao = tao_balance.rao / (sqrt_current_price - sqrt_price_low) - + # Calculate liquidity from Alpha: L = alpha / (1/sqrt_current_price - 1/sqrt_price_high) liquidity_from_alpha = alpha_balance.rao / ( 1 / sqrt_current_price - 1 / sqrt_price_high ) - + # Maximum liquidity is limited by the smaller of the two max_liquidity_rao = min(liquidity_from_tao, liquidity_from_alpha) - + # Calculate the actual amounts needed tao_needed_rao = max_liquidity_rao * (sqrt_current_price - sqrt_price_low) alpha_needed_rao = max_liquidity_rao * ( 1 / sqrt_current_price - 1 / sqrt_price_high ) - + return ( Balance.from_rao(int(max_liquidity_rao)), Balance.from_rao(int(tao_needed_rao)), @@ -281,34 +283,34 @@ def calculate_alpha_from_tao( price_high: Balance, ) -> Balance: """Calculate the Alpha amount needed for a given TAO amount. - + Arguments: tao_amount: TAO amount to provide current_price: Current subnet price (Alpha/TAO) price_low: Lower bound of the price range price_high: Upper bound of the price range - + Returns: Balance: Alpha amount needed """ sqrt_price_low = math.sqrt(price_low.tao) sqrt_price_high = math.sqrt(price_high.tao) sqrt_current_price = math.sqrt(current_price.tao) - + # If current price is below range, no TAO should be provided if sqrt_current_price < sqrt_price_low: return Balance.from_rao(0) - + # If current price is above range, no Alpha is needed if sqrt_current_price > sqrt_price_high: return Balance.from_rao(0) - + # Calculate liquidity from TAO liquidity_rao = tao_amount.rao / (sqrt_current_price - sqrt_price_low) - + # Calculate Alpha needed for this liquidity alpha_needed_rao = liquidity_rao * (1 / sqrt_current_price - 1 / sqrt_price_high) - + return Balance.from_rao(int(alpha_needed_rao)) @@ -319,32 +321,32 @@ def calculate_tao_from_alpha( price_high: Balance, ) -> Balance: """Calculate the TAO amount needed for a given Alpha amount. - + Arguments: alpha_amount: Alpha amount to provide current_price: Current subnet price (Alpha/TAO) price_low: Lower bound of the price range price_high: Upper bound of the price range - + Returns: Balance: TAO amount needed """ sqrt_price_low = math.sqrt(price_low.tao) sqrt_price_high = math.sqrt(price_high.tao) sqrt_current_price = math.sqrt(current_price.tao) - + # If current price is above range, no Alpha should be provided if sqrt_current_price > sqrt_price_high: return Balance.from_rao(0) - + # If current price is below range, no TAO is needed if sqrt_current_price < sqrt_price_low: return Balance.from_rao(0) - + # Calculate liquidity from Alpha liquidity_rao = alpha_amount.rao / (1 / sqrt_current_price - 1 / sqrt_price_high) - + # Calculate TAO needed for this liquidity tao_needed_rao = liquidity_rao * (sqrt_current_price - sqrt_price_low) - + return Balance.from_rao(int(tao_needed_rao)) diff --git a/tests/unit_tests/test_liquidity_utils.py b/tests/unit_tests/test_liquidity_utils.py index fe5e3cdaf..3337b8afc 100644 --- a/tests/unit_tests/test_liquidity_utils.py +++ b/tests/unit_tests/test_liquidity_utils.py @@ -1,4 +1,5 @@ """Unit tests for liquidity utility functions.""" + import math import pytest from bittensor_cli.src.bittensor.balances import Balance @@ -64,7 +65,9 @@ def test_calculate_max_liquidity_both_needed(self): assert max_liquidity.rao > 0, "Liquidity should be calculated" # Should not exceed available balances assert max_tao.rao <= tao_balance.rao, "TAO needed should not exceed balance" - assert max_alpha.rao <= alpha_balance.rao, "Alpha needed should not exceed balance" + assert max_alpha.rao <= alpha_balance.rao, ( + "Alpha needed should not exceed balance" + ) def test_calculate_alpha_from_tao_within_range(self): """Test calculating Alpha amount from TAO when price is within range.""" @@ -162,5 +165,6 @@ def test_reciprocal_calculation(self): ) # Should be approximately equal (within rounding error) - assert abs(tao_back.rao - tao_amount.rao) < 1000, \ + assert abs(tao_back.rao - tao_amount.rao) < 1000, ( "Reciprocal calculation should yield similar result" + )