"""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)