Source code for tradeexecutor.strategy.freqtrade.freqtrade_routing

"""Freqtrade routing model for deposit/withdrawal management."""

import logging
import time
from dataclasses import dataclass
from decimal import Decimal

from hexbytes import HexBytes
from web3 import Web3

from eth_defi.abi import get_deployed_contract
from eth_defi.token import fetch_erc20_details

from tradeexecutor.ethereum.tx import TransactionBuilder
from tradeexecutor.state.blockhain_transaction import BlockchainTransaction
from tradeexecutor.state.state import State
from tradeexecutor.state.trade import TradeExecution
from tradeexecutor.strategy.routing import RoutingModel, RoutingState
from tradeexecutor.strategy.universe_model import StrategyExecutionUniverse
from tradeexecutor.strategy.freqtrade.config import (
    FreqtradeConfig,

    OnChainTransferExchangeConfig,
    AsterExchangeConfig,
    HyperliquidExchangeConfig,
    OrderlyExchangeConfig,
    HYPERLIQUID_BRIDGE_MAINNET,
    HYPERLIQUID_BRIDGE_TESTNET,
    USDC_ARBITRUM_MAINNET,
    USDC_ARBITRUM_TESTNET,
)
from tradeexecutor.strategy.freqtrade.freqtrade_client import FreqtradeClient

logger = logging.getLogger(__name__)


[docs]@dataclass class FreqtradeRoutingState(RoutingState): """Routing state for Freqtrade deposits. Holds tx_builder and FreqtradeClient instances for the execution cycle. """ tx_builder: TransactionBuilder | None web3: Web3 | None freqtrade_clients: dict[str, FreqtradeClient]
[docs]class FreqtradeRoutingModel(RoutingModel): """Route capital deposits/withdrawals to Freqtrade instances. Supports multiple deposit methods: - On-chain transfer: Simple ERC20 transfer (for Lagoon vault integration) - Aster: ERC20 approve + AstherusVault.deposit() on BSC - Hyperliquid: Bridge transfer on Arbitrum + SDK vault deposit - Orderly vault: ERC20 approve + Vault.deposit() with hashed params """
[docs] def __init__(self, freqtrade_configs: dict[str, FreqtradeConfig]): """Initialise routing model. Args: freqtrade_configs: Dict mapping freqtrade_id -> FreqtradeConfig """ # Freqtrade doesn't use DEX routing - pass empty intermediary pairs super().__init__( allowed_intermediary_pairs={}, reserve_token_address="0x0000000000000000000000000000000000000000", ) self.freqtrade_configs = freqtrade_configs
[docs] def create_routing_state( self, universe: StrategyExecutionUniverse, execution_details: object, ) -> FreqtradeRoutingState: """Create routing state for this cycle. Args: universe: Strategy execution universe execution_details: Dict containing tx_builder from ExecutionModel Returns: FreqtradeRoutingState with tx_builder and FreqtradeClients """ tx_builder = None web3 = None if execution_details: tx_builder = execution_details.get("tx_builder") if tx_builder: web3 = tx_builder.web3 # Create FreqtradeClient for each config clients = {} for freqtrade_id, config in self.freqtrade_configs.items(): clients[freqtrade_id] = FreqtradeClient( config.api_url, config.api_username, config.api_password, ) return FreqtradeRoutingState( tx_builder=tx_builder, web3=web3, freqtrade_clients=clients, )
[docs] def setup_trades( self, state: State, routing_state: FreqtradeRoutingState, trades: list[TradeExecution], check_balances: bool = False, rebroadcast: bool = False, **kwargs, ): """Prepare deposit or withdrawal transactions. Dispatches to method-specific builders based on trade direction and exchange config. Args: state: Current portfolio state routing_state: Routing state with tx_builder and clients trades: Trades to prepare check_balances: Whether to check balances (not used) rebroadcast: Whether this is a rebroadcast (not used) **kwargs: Additional arguments """ for trade in trades: freqtrade_id = trade.pair.other_data["freqtrade_id"] config = self.freqtrade_configs[freqtrade_id] exchange_config = config.exchange assert exchange_config, f"Exchange config required for {freqtrade_id}" # Dispatch by trade direction and exchange config type if trade.is_buy(): # Deposit flow if isinstance(exchange_config, OnChainTransferExchangeConfig): txs = self._build_on_chain_transfer_deposit_tx(trade, config, exchange_config, routing_state) elif isinstance(exchange_config, AsterExchangeConfig): txs = self._build_aster_deposit_tx(trade, config, exchange_config, routing_state) elif isinstance(exchange_config, HyperliquidExchangeConfig): txs = self._build_hyperliquid_deposit_tx(trade, config, exchange_config, routing_state) elif isinstance(exchange_config, OrderlyExchangeConfig): txs = self._build_orderly_deposit_tx(trade, config, exchange_config, routing_state) else: raise NotImplementedError( f"Deposit for exchange config type {type(exchange_config).__name__} not implemented" ) else: # Withdrawal flow if isinstance(exchange_config, OnChainTransferExchangeConfig): txs = self._build_on_chain_transfer_withdrawal_tx(trade, config, exchange_config, routing_state) elif isinstance(exchange_config, AsterExchangeConfig): txs = self._build_aster_withdrawal_tx(trade, config, exchange_config, routing_state) elif isinstance(exchange_config, HyperliquidExchangeConfig): txs = self._build_hyperliquid_withdrawal_tx(trade, config, exchange_config, routing_state) elif isinstance(exchange_config, OrderlyExchangeConfig): txs = self._build_orderly_withdrawal_tx(trade, config, exchange_config, routing_state) else: raise NotImplementedError( f"Withdrawal for exchange config type {type(exchange_config).__name__} not implemented" ) trade.blockchain_transactions = txs
def _build_on_chain_transfer_deposit_tx( self, trade: TradeExecution, config: FreqtradeConfig, exchange_config: OnChainTransferExchangeConfig, routing_state: FreqtradeRoutingState, ) -> list[BlockchainTransaction]: """Build simple ERC20 transfer transaction. Used for Lagoon vault integration where the vault cannot sign transactions directly, requiring a wallet-in-the-middle. Args: trade: Trade to build transaction for config: Freqtrade configuration deposit_config: On-chain transfer configuration routing_state: Routing state with tx_builder Returns: List containing single transfer transaction """ if routing_state.tx_builder is None: raise ValueError("tx_builder required for on_chain_transfer deposits") if routing_state.web3 is None: raise ValueError("web3 required for on_chain_transfer deposits") if exchange_config.recipient_address is None: raise ValueError(f"deposit.recipient_address required for {config.freqtrade_id}") web3 = routing_state.web3 # Get Freqtrade balance before deposit client = routing_state.freqtrade_clients[config.freqtrade_id] balance_before = Decimal(str(client.get_balance().get("total", 0))) # Store balance_before in trade for later verification if trade.other_data is None: trade.other_data = {} trade.other_data["balance_before_deposit"] = str(balance_before) # Get token details and build transfer amount = trade.planned_reserve if trade.planned_reserve else trade.planned_quantity token = fetch_erc20_details(web3, config.reserve_currency) amount_raw = token.convert_to_raw(amount) recipient = Web3.to_checksum_address(exchange_config.recipient_address) transfer_call = token.contract.functions.transfer(recipient, amount_raw) transfer_tx = routing_state.tx_builder.sign_transaction( token.contract, transfer_call, gas_limit=100_000, notes=f"On-chain transfer for {config.freqtrade_id}", ) trade.notes = f"On-chain transfer: {amount} to {recipient}" logger.info(f"Trade {trade.trade_id}: {trade.notes}") return [transfer_tx] def _build_aster_deposit_tx( self, trade: TradeExecution, config: FreqtradeConfig, exchange_config: AsterExchangeConfig, routing_state: FreqtradeRoutingState, ) -> list[BlockchainTransaction]: """Build Aster deposit transactions. Flow: 1. ERC20.approve(vault_address, amount) 2. AstherusVault.deposit(token_address, amount, broker_id) Args: trade: Trade to build transaction for config: Freqtrade configuration deposit_config: Aster deposit configuration routing_state: Routing state with tx_builder Returns: List of BlockchainTransactions (approve + deposit) """ if routing_state.tx_builder is None: raise ValueError("tx_builder required for aster deposits") if routing_state.web3 is None: raise ValueError("web3 required for aster deposits") if exchange_config.vault_address is None: raise ValueError(f"deposit.vault_address required for {config.freqtrade_id}") web3 = routing_state.web3 # Get Freqtrade balance before deposit client = routing_state.freqtrade_clients[config.freqtrade_id] balance_before = Decimal(str(client.get_balance().get("total", 0))) # Store balance_before in trade for later verification if trade.other_data is None: trade.other_data = {} trade.other_data["balance_before_deposit"] = str(balance_before) # Get token details amount = trade.planned_reserve if trade.planned_reserve else trade.planned_quantity token = fetch_erc20_details(web3, config.reserve_currency) amount_raw = token.convert_to_raw(amount) vault_address = Web3.to_checksum_address(exchange_config.vault_address) # 1. Build approve transaction approve_call = token.contract.functions.approve(vault_address, amount_raw) approve_tx = routing_state.tx_builder.sign_transaction( token.contract, approve_call, gas_limit=100_000, notes=f"Approve Aster for {config.freqtrade_id}", ) # 2. Build vault.deposit transaction vault = get_deployed_contract( web3, "aster/AstherusVault.json", vault_address, ) deposit_call = vault.functions.deposit( Web3.to_checksum_address(config.reserve_currency), amount_raw, exchange_config.broker_id, ) deposit_tx = routing_state.tx_builder.sign_transaction( vault, deposit_call, gas_limit=200_000, notes=f"Aster deposit for {config.freqtrade_id}", ) trade.notes = ( f"Aster deposit: {amount} to {vault_address}" ) logger.info(f"Trade {trade.trade_id}: {trade.notes}") return [approve_tx, deposit_tx] def _build_hyperliquid_deposit_tx( self, trade: TradeExecution, config: FreqtradeConfig, exchange_config: HyperliquidExchangeConfig, routing_state: FreqtradeRoutingState, ) -> list[BlockchainTransaction]: """Build Hyperliquid bridge transfer transaction. On-chain part only: Transfer USDC to bridge on Arbitrum. The off-chain SDK vault deposit happens in settle_trade(). Args: trade: Trade to build transaction for config: Freqtrade configuration deposit_config: Hyperliquid deposit configuration routing_state: Routing state with tx_builder Returns: List containing bridge transfer transaction """ if routing_state.tx_builder is None: raise ValueError("tx_builder required for hyperliquid deposits") if routing_state.web3 is None: raise ValueError("web3 required for hyperliquid deposits") if exchange_config.vault_address is None: raise ValueError(f"deposit.vault_address required for {config.freqtrade_id}") web3 = routing_state.web3 # Get Freqtrade balance before deposit client = routing_state.freqtrade_clients[config.freqtrade_id] balance_before = Decimal(str(client.get_balance().get("total", 0))) # Store balance_before and vault_address in trade for settle_trade if trade.other_data is None: trade.other_data = {} trade.other_data["balance_before_deposit"] = str(balance_before) trade.other_data["hyperliquid_vault_address"] = exchange_config.vault_address trade.other_data["hyperliquid_is_mainnet"] = exchange_config.is_mainnet # Get bridge and USDC addresses based on network if exchange_config.is_mainnet: bridge_address = HYPERLIQUID_BRIDGE_MAINNET usdc_address = USDC_ARBITRUM_MAINNET else: bridge_address = HYPERLIQUID_BRIDGE_TESTNET usdc_address = USDC_ARBITRUM_TESTNET # Build transfer to bridge (USDC on Arbitrum) amount = trade.planned_reserve if trade.planned_reserve else trade.planned_quantity token = fetch_erc20_details(web3, usdc_address) amount_raw = token.convert_to_raw(amount) transfer_call = token.contract.functions.transfer( Web3.to_checksum_address(bridge_address), amount_raw, ) transfer_tx = routing_state.tx_builder.sign_transaction( token.contract, transfer_call, gas_limit=100_000, notes=f"Hyperliquid bridge transfer for {config.freqtrade_id}", ) trade.notes = ( f"Hyperliquid bridge transfer: {amount} USDC to {bridge_address}" ) logger.info(f"Trade {trade.trade_id}: {trade.notes}") return [transfer_tx] def _build_orderly_deposit_tx( self, trade: TradeExecution, config: FreqtradeConfig, exchange_config: OrderlyExchangeConfig, routing_state: FreqtradeRoutingState, ) -> list[BlockchainTransaction]: """Build Orderly vault deposit transactions. Flow: 1. ERC20.approve(vault_address, amount) 2. Vault.deposit((account_id, broker_hash, token_hash, amount)) Args: trade: Trade to build transaction for config: Freqtrade configuration deposit_config: Orderly deposit configuration routing_state: Routing state with tx_builder Returns: List of BlockchainTransactions (approve + deposit) """ if routing_state.tx_builder is None: raise ValueError("tx_builder required for orderly_vault deposits") if routing_state.web3 is None: raise ValueError("web3 required for orderly_vault deposits") if exchange_config.vault_address is None: raise ValueError(f"deposit.vault_address required for {config.freqtrade_id}") if exchange_config.orderly_account_id is None: raise ValueError(f"deposit.orderly_account_id required for {config.freqtrade_id}") if exchange_config.broker_id is None: raise ValueError(f"deposit.broker_id required for {config.freqtrade_id}") web3 = routing_state.web3 # Get Freqtrade balance before deposit client = routing_state.freqtrade_clients[config.freqtrade_id] balance_before = Decimal(str(client.get_balance().get("total", 0))) # Store balance_before in trade for later verification if trade.other_data is None: trade.other_data = {} trade.other_data["balance_before_deposit"] = str(balance_before) # Get token details amount = trade.planned_reserve if trade.planned_reserve else trade.planned_quantity token = fetch_erc20_details(web3, config.reserve_currency) amount_raw = token.convert_to_raw(amount) vault_address = Web3.to_checksum_address(exchange_config.vault_address) # 1. Build approve transaction approve_call = token.contract.functions.approve(vault_address, amount_raw) approve_tx = routing_state.tx_builder.sign_transaction( token.contract, approve_call, gas_limit=100_000, notes=f"Approve Orderly vault for {config.freqtrade_id}", ) # 2. Build vault.deposit transaction with hashed parameters broker_hash = web3.keccak(text=exchange_config.broker_id) token_hash = web3.keccak(text=exchange_config.token_id) if exchange_config.token_id else bytes(32) # Build deposit input tuple deposit_input = ( bytes.fromhex(exchange_config.orderly_account_id[2:]), # Remove 0x prefix broker_hash, token_hash, amount_raw, ) vault = get_deployed_contract( web3, "orderly/Vault.json", vault_address, ) deposit_call = vault.functions.deposit(deposit_input) deposit_tx = routing_state.tx_builder.sign_transaction( vault, deposit_call, gas_limit=200_000, notes=f"Orderly vault deposit for {config.freqtrade_id}", ) trade.notes = ( f"Orderly vault deposit: {amount} to {vault_address}" ) logger.info(f"Trade {trade.trade_id}: {trade.notes}") return [approve_tx, deposit_tx] def _build_on_chain_transfer_withdrawal_tx( self, trade: TradeExecution, config: FreqtradeConfig, exchange_config: OnChainTransferExchangeConfig, routing_state: FreqtradeRoutingState, ) -> list[BlockchainTransaction]: """Build withdrawal placeholder for on-chain transfer. For on-chain transfer withdrawals, no transaction is built because the withdrawal is initiated externally (via CEX/exchange API). We only wait for the balance decrease confirmation in settle_trade(). Args: trade: Trade to build transaction for config: Freqtrade configuration exchange_config: On-chain transfer configuration routing_state: Routing state with tx_builder Returns: Empty list (no transactions to broadcast) """ # Get Freqtrade balance before withdrawal client = routing_state.freqtrade_clients[config.freqtrade_id] balance_before = Decimal(str(client.get_balance().get("total", 0))) # Store balance_before in trade for later verification if trade.other_data is None: trade.other_data = {} trade.other_data["balance_before_withdrawal"] = str(balance_before) amount = trade.planned_reserve if trade.planned_reserve else abs(trade.planned_quantity) recipient = exchange_config.recipient_address trade.notes = f"On-chain transfer withdrawal: {amount} to {recipient}" logger.info(f"Trade {trade.trade_id}: {trade.notes}") return [] def _build_aster_withdrawal_tx( self, trade: TradeExecution, config: FreqtradeConfig, exchange_config: AsterExchangeConfig, routing_state: FreqtradeRoutingState, ) -> list[BlockchainTransaction]: """Build Aster withdrawal transaction. Aster withdrawals require signed messages or validator signatures, which is not yet implemented. This is a placeholder that raises NotImplementedError. Args: trade: Trade to build transaction for config: Freqtrade configuration exchange_config: Aster configuration routing_state: Routing state with tx_builder Raises: NotImplementedError: Always raised as Aster withdrawal is not implemented """ raise NotImplementedError( "Aster withdrawal requires signed message or validator signatures - " "signature infrastructure not yet implemented" ) def _build_hyperliquid_withdrawal_tx( self, trade: TradeExecution, config: FreqtradeConfig, exchange_config: HyperliquidExchangeConfig, routing_state: FreqtradeRoutingState, ) -> list[BlockchainTransaction]: """Build Hyperliquid withdrawal placeholder. For Hyperliquid withdrawals, no on-chain transaction is built because the SDK handles the off-chain withdrawal. The actual SDK call happens in settle_trade() via _confirm_hyperliquid_withdrawal(). Args: trade: Trade to build transaction for config: Freqtrade configuration exchange_config: Hyperliquid configuration routing_state: Routing state with tx_builder Returns: Empty list (no transactions to broadcast) """ # Get Freqtrade balance before withdrawal client = routing_state.freqtrade_clients[config.freqtrade_id] balance_before = Decimal(str(client.get_balance().get("total", 0))) # Store balance_before and vault_address in trade for settle_trade if trade.other_data is None: trade.other_data = {} trade.other_data["balance_before_withdrawal"] = str(balance_before) trade.other_data["hyperliquid_vault_address"] = exchange_config.vault_address trade.other_data["hyperliquid_is_mainnet"] = exchange_config.is_mainnet amount = trade.planned_reserve if trade.planned_reserve else abs(trade.planned_quantity) trade.notes = f"Hyperliquid withdrawal: {amount} USD from vault {exchange_config.vault_address}" logger.info(f"Trade {trade.trade_id}: {trade.notes}") return [] def _build_orderly_withdrawal_tx( self, trade: TradeExecution, config: FreqtradeConfig, exchange_config: OrderlyExchangeConfig, routing_state: FreqtradeRoutingState, ) -> list[BlockchainTransaction]: """Build Orderly vault withdrawal transaction. Flow: 1. Vault.withdraw((account_id, broker_hash, token_hash, amount)) Args: trade: Trade to build transaction for config: Freqtrade configuration exchange_config: Orderly configuration routing_state: Routing state with tx_builder Returns: List containing withdraw transaction """ if routing_state.tx_builder is None: raise ValueError("tx_builder required for orderly_vault withdrawals") if routing_state.web3 is None: raise ValueError("web3 required for orderly_vault withdrawals") web3 = routing_state.web3 # Get Freqtrade balance before withdrawal client = routing_state.freqtrade_clients[config.freqtrade_id] balance_before = Decimal(str(client.get_balance().get("total", 0))) # Store balance_before in trade for later verification if trade.other_data is None: trade.other_data = {} trade.other_data["balance_before_withdrawal"] = str(balance_before) # Get token details amount = trade.planned_reserve if trade.planned_reserve else abs(trade.planned_quantity) token = fetch_erc20_details(web3, config.reserve_currency) amount_raw = token.convert_to_raw(amount) vault_address = Web3.to_checksum_address(exchange_config.vault_address) # Build vault.withdraw transaction with hashed parameters broker_hash = web3.keccak(text=exchange_config.broker_id) token_hash = web3.keccak(text=exchange_config.token_id) if exchange_config.token_id else bytes(32) # Build withdraw input tuple (same structure as deposit) withdraw_input = ( bytes.fromhex(exchange_config.orderly_account_id[2:]), # Remove 0x prefix broker_hash, token_hash, amount_raw, ) vault = get_deployed_contract( web3, "orderly/Vault.json", vault_address, ) withdraw_call = vault.functions.withdraw(withdraw_input) withdraw_tx = routing_state.tx_builder.sign_transaction( vault, withdraw_call, gas_limit=200_000, notes=f"Orderly vault withdrawal for {config.freqtrade_id}", ) trade.notes = ( f"Orderly vault withdrawal: {amount} from {vault_address}" ) logger.info(f"Trade {trade.trade_id}: {trade.notes}") return [withdraw_tx]
[docs] def settle_trade( self, web3: Web3, state: State, trade: TradeExecution, receipts: dict[HexBytes, dict], stop_on_execution_failure: bool = False, **kwargs, ): """Settle a trade after transaction broadcast. For deposits: polls Freqtrade balance until deposit confirmed. For withdrawals: polls Freqtrade balance until withdrawal confirmed. For Hyperliquid: also performs SDK vault transfer (deposit or withdrawal). Args: web3: Web3 instance state: Current portfolio state trade: Trade to settle receipts: Transaction receipts stop_on_execution_failure: Whether to stop on failure **kwargs: Additional arguments """ freqtrade_id = trade.pair.other_data["freqtrade_id"] config = self.freqtrade_configs[freqtrade_id] exchange_config = config.exchange assert exchange_config, f"Exchange config required for {freqtrade_id}" # Dispatch by trade direction and exchange config type if trade.is_buy(): # Deposit confirmation if isinstance(exchange_config, OnChainTransferExchangeConfig): self._confirm_deposit(trade, config, exchange_config) elif isinstance(exchange_config, AsterExchangeConfig): self._confirm_deposit(trade, config, exchange_config) elif isinstance(exchange_config, HyperliquidExchangeConfig): self._confirm_hyperliquid_deposit(trade, config, exchange_config, **kwargs) elif isinstance(exchange_config, OrderlyExchangeConfig): self._confirm_deposit(trade, config, exchange_config) else: raise NotImplementedError( f"Deposit confirmation for {type(exchange_config).__name__} not implemented" ) else: # Withdrawal confirmation if isinstance(exchange_config, OnChainTransferExchangeConfig): self._confirm_withdrawal(trade, config, exchange_config) elif isinstance(exchange_config, AsterExchangeConfig): raise NotImplementedError( "Aster withdrawal requires signed message/validator signatures - not yet implemented" ) elif isinstance(exchange_config, HyperliquidExchangeConfig): self._confirm_hyperliquid_withdrawal(trade, config, exchange_config, **kwargs) elif isinstance(exchange_config, OrderlyExchangeConfig): self._confirm_withdrawal(trade, config, exchange_config) else: raise NotImplementedError( f"Withdrawal confirmation for {type(exchange_config).__name__} not implemented" )
def _confirm_deposit( self, trade: TradeExecution, config: FreqtradeConfig, exchange_config: OnChainTransferExchangeConfig | AsterExchangeConfig | OrderlyExchangeConfig, ): """Poll Freqtrade balance until deposit is confirmed. Works for on-chain transfer, Aster, and Orderly vault deposits. Args: trade: Trade containing balance_before_deposit config: Freqtrade configuration exchange_config: Exchange configuration with timeout settings Raises: Exception: If deposit not confirmed within timeout """ client = FreqtradeClient( config.api_url, config.api_username, config.api_password, ) balance_before = Decimal(trade.other_data.get("balance_before_deposit", "0")) amount = trade.planned_reserve if trade.planned_reserve else trade.planned_quantity expected_min = balance_before + amount - exchange_config.fee_tolerance deadline = time.time() + exchange_config.confirmation_timeout logger.info( f"Trade {trade.trade_id}: Waiting for deposit confirmation. " f"Expected balance >= {expected_min}" ) balance_after = None while time.time() < deadline: try: balance_after = Decimal(str(client.get_balance().get("total", 0))) if balance_after >= expected_min: trade.notes = f"Deposit confirmed: {balance_before} -> {balance_after}" logger.info(f"Trade {trade.trade_id}: {trade.notes}") return logger.debug( f"Trade {trade.trade_id}: Balance {balance_after}, " f"waiting for {expected_min}" ) except Exception as e: logger.warning(f"Trade {trade.trade_id}: Balance check failed: {e}") time.sleep(exchange_config.poll_interval) raise Exception( f"Deposit not confirmed within {exchange_config.confirmation_timeout}s. " f"Expected balance >= {expected_min}, got {balance_after}" ) def _confirm_hyperliquid_deposit( self, trade: TradeExecution, config: FreqtradeConfig, exchange_config: HyperliquidExchangeConfig, **kwargs, ): """Perform Hyperliquid SDK vault deposit and confirm. After bridge transfer, this: 1. Calls SDK vault_usd_transfer() to deposit into vault 2. Polls Freqtrade balance until confirmed Args: trade: Trade containing vault info config: Freqtrade configuration exchange_config: Hyperliquid exchange configuration **kwargs: May contain 'private_key' for SDK signing Raises: Exception: If deposit not confirmed within timeout """ try: from eth_account import Account from hyperliquid.exchange import Exchange from hyperliquid.utils.constants import MAINNET_API_URL, TESTNET_API_URL except ImportError: raise ImportError( "hyperliquid-py package required for Hyperliquid deposits. " "Install with: pip install hyperliquid-py" ) vault_address = trade.other_data.get("hyperliquid_vault_address") is_mainnet = trade.other_data.get("hyperliquid_is_mainnet", True) # Get private key for SDK signing private_key = kwargs.get("private_key") if private_key is None: raise ValueError("private_key required for Hyperliquid SDK vault deposit") # Perform SDK vault deposit wallet = Account.from_key(private_key) base_url = MAINNET_API_URL if is_mainnet else TESTNET_API_URL exchange = Exchange(wallet=wallet, base_url=base_url) amount = trade.planned_reserve if trade.planned_reserve else trade.planned_quantity amount_int = int(amount * Decimal("1000000")) # Convert to micro-USD logger.info( f"Trade {trade.trade_id}: Performing Hyperliquid SDK vault deposit. " f"Vault: {vault_address}, Amount: {amount} USD" ) result = exchange.vault_usd_transfer( vault_address=vault_address, is_deposit=True, usd=amount_int, ) if result.get("status") != "ok": raise Exception(f"Hyperliquid vault deposit failed: {result}") logger.info(f"Trade {trade.trade_id}: SDK vault deposit successful") # Now confirm via Freqtrade balance polling self._confirm_deposit(trade, config, exchange_config) def _confirm_withdrawal( self, trade: TradeExecution, config: FreqtradeConfig, exchange_config: OnChainTransferExchangeConfig | OrderlyExchangeConfig, ): """Poll Freqtrade balance until withdrawal is confirmed. Works for on-chain transfer and Orderly vault withdrawals. Args: trade: Trade containing balance_before_withdrawal config: Freqtrade configuration exchange_config: Exchange configuration with timeout settings Raises: Exception: If withdrawal not confirmed within timeout """ client = FreqtradeClient( config.api_url, config.api_username, config.api_password, ) balance_before = Decimal(trade.other_data.get("balance_before_withdrawal", "0")) amount = trade.planned_reserve if trade.planned_reserve else abs(trade.planned_quantity) expected_max = balance_before - amount + exchange_config.fee_tolerance deadline = time.time() + exchange_config.confirmation_timeout logger.info( f"Trade {trade.trade_id}: Waiting for withdrawal confirmation. " f"Expected balance <= {expected_max}" ) balance_after = None while time.time() < deadline: try: balance_after = Decimal(str(client.get_balance().get("total", 0))) if balance_after <= expected_max: trade.notes = f"Withdrawal confirmed: {balance_before} -> {balance_after}" logger.info(f"Trade {trade.trade_id}: {trade.notes}") return logger.debug( f"Trade {trade.trade_id}: Balance {balance_after}, " f"waiting for {expected_max}" ) except Exception as e: logger.warning(f"Trade {trade.trade_id}: Balance check failed: {e}") time.sleep(exchange_config.poll_interval) raise Exception( f"Withdrawal not confirmed within {exchange_config.confirmation_timeout}s. " f"Expected balance <= {expected_max}, got {balance_after}" ) def _confirm_hyperliquid_withdrawal( self, trade: TradeExecution, config: FreqtradeConfig, exchange_config: HyperliquidExchangeConfig, **kwargs, ): """Perform Hyperliquid SDK vault withdrawal and confirm. This: 1. Calls SDK vault_usd_transfer(is_deposit=False) to withdraw from vault 2. Polls Freqtrade balance until confirmed Args: trade: Trade containing vault info config: Freqtrade configuration exchange_config: Hyperliquid exchange configuration **kwargs: May contain 'private_key' for SDK signing Raises: Exception: If withdrawal not confirmed within timeout """ try: from eth_account import Account from hyperliquid.exchange import Exchange from hyperliquid.utils.constants import MAINNET_API_URL, TESTNET_API_URL except ImportError: raise ImportError( "hyperliquid-py package required for Hyperliquid withdrawals. " "Install with: pip install hyperliquid-py" ) vault_address = trade.other_data.get("hyperliquid_vault_address") is_mainnet = trade.other_data.get("hyperliquid_is_mainnet", True) # Get private key for SDK signing private_key = kwargs.get("private_key") if private_key is None: raise ValueError("private_key required for Hyperliquid SDK vault withdrawal") # Perform SDK vault withdrawal wallet = Account.from_key(private_key) base_url = MAINNET_API_URL if is_mainnet else TESTNET_API_URL exchange = Exchange(wallet=wallet, base_url=base_url) amount = trade.planned_reserve if trade.planned_reserve else abs(trade.planned_quantity) amount_int = int(amount * Decimal("1000000")) # Convert to micro-USD logger.info( f"Trade {trade.trade_id}: Performing Hyperliquid SDK vault withdrawal. " f"Vault: {vault_address}, Amount: {amount} USD" ) result = exchange.vault_usd_transfer( vault_address=vault_address, is_deposit=False, # Withdrawal usd=amount_int, ) if result.get("status") != "ok": raise Exception(f"Hyperliquid vault withdrawal failed: {result}") logger.info(f"Trade {trade.trade_id}: SDK vault withdrawal successful") # Now confirm via Freqtrade balance polling self._confirm_withdrawal(trade, config, exchange_config)