diff --git a/.gitignore b/.gitignore index 64ad9ca2..dcfc78e0 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,5 @@ bots/conf/ .idea/ improvements bots/gateway-files/ +.DS_Store +Dockerfile.patched-hummingbot diff --git a/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/.password_verification b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/.password_verification new file mode 100644 index 00000000..d1a274b6 --- /dev/null +++ b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/.password_verification @@ -0,0 +1 @@ +7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20226463613937613132666365383432623664323437613830393966313638373662227d2c202263697068657274657874223a20223537393331373032636666343037396439623430222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226536376664366338373033386336613439653066373938316366656138656539227d2c20226d6163223a202233366134323163646334303464393433343264656166613037623235373866363734383930653961393765303037393231333132326532333464303666323034227d2c202276657273696f6e223a20332c2022616c696173223a2022227d \ No newline at end of file diff --git a/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/conf_client.yml b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/conf_client.yml new file mode 100644 index 00000000..37833163 --- /dev/null +++ b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/conf_client.yml @@ -0,0 +1,179 @@ +#################################### +### client_config_map config ### +#################################### + +instance_id: pmm_USDC-XRP_20260807-20260807-145233 + +# Fetch trading pairs from all exchanges if True, otherwise fetch only from connected exchanges. +fetch_pairs_from_all_exchanges: false + +log_level: INFO + +debug_console: false + +strategy_report_interval: 900.0 + +logger_override_whitelist: +- hummingbot.strategy.arbitrage +- hummingbot.strategy.cross_exchange_market_making +- conf + +log_file_path: /home/hummingbot/logs + +kill_switch_mode: {} + +# What to auto-fill in the prompt after each import command (start/config) +autofill_import: disabled + +# MQTT Bridge configuration. +mqtt_bridge: + mqtt_host: localhost + mqtt_port: 1883 + mqtt_username: '' + mqtt_password: '' + mqtt_namespace: hbot + mqtt_ssl: false + mqtt_logger: true + mqtt_notifier: true + mqtt_commands: true + mqtt_events: true + mqtt_external_events: true + mqtt_autostart: true + +# Error log sharing +send_error_logs: true + +# Advanced database options, currently supports SQLAlchemy's included dialects +# Reference: https://docs.sqlalchemy.org/en/13/dialects/ +# To use an instance of SQLite DB the required configuration is +# db_engine: sqlite +# To use a DBMS the required configuration is +# db_host: 127.0.0.1 +# db_port: 3306 +# db_username: username +# db_password: password +# db_name: dbname +db_mode: + db_engine: sqlite + +# Balance Limit Configurations +# e.g. Setting USDT and BTC limits on Binance. +# balance_asset_limit: +# binance: +# BTC: 0.1 +# USDT: 1000 +balance_asset_limit: + ascend_ex: {} + ascend_ex_paper_trade: {} + binance: {} + binance_paper_trade: {} + binance_us: {} + bitfinex: {} + bitmart: {} + bitmex: {} + bitmex_testnet: {} + btc_markets: {} + bybit: {} + bybit_testnet: {} + coinbase_pro: {} + foxbit: {} + gate_io: {} + gate_io_paper_trade: {} + hitbtc: {} + huobi: {} + injective_v2: {} + kraken: {} + kucoin: {} + kucoin_paper_trade: {} + mexc: {} + mock_paper_exchange: {} + ndax: {} + ndax_testnet: {} + okx: {} + polkadex: {} + vertex: {} + vertex_testnet: {} + woo_x: {} + woo_x_testnet: {} + +# Fixed gas price (in Gwei) for Ethereum transactions +manual_gas_price: 50.0 + +# Gateway API Configurations +# default host to only use localhost +# Port need to match the final installation port for Gateway +gateway: + gateway_api_host: localhost + gateway_api_port: '15888' + gateway_use_ssl: true + +# Whether to enable aggregated order and trade data collection +anonymized_metrics_mode: + anonymized_metrics_interval_min: 15.0 + +# A source for rate oracle, currently binance, coin_gecko, coin_cap, dexalot, evedex_perpetual, decibel_perpetual, kucoin, gate_io, backpack, coinbase_advanced_trade, hyperliquid, hyperliquid_perpetual, architect_perpetual, derive, mexc +rate_oracle_source: + name: gate_io + +# A universal token which to display tokens values in, e.g. USD,EUR,BTC +global_token: + global_token_name: USDT + global_token_symbol: $ + usd_equivalent_tokens: + - USD + +# Percentage of API rate limits (on any exchange and any end point) allocated to this bot instance. +# Enter 50 to indicate 50%. E.g. if the API rate limit is 100 calls per second, and you allocate +# 50% to this setting, the bot will have a maximum (limit) of 50 calls per second +rate_limits_share_pct: 100.0 + +commands_timeout: + create_command_timeout: 10.0 + other_commands_timeout: 30.0 + +# Tabulate table format style (https://github.com/astanin/python-tabulate#table-format) +tables_format: psql + +paper_trade: + paper_trade_exchanges: + - binance + - kucoin + - ascend_ex + - gate_io + paper_trade_account_balance: + BTC: 1.0 + DAI: 1000.0 + ETH: 10.0 + ONE: 1000.0 + TUSD: 1000.0 + USDC: 1000.0 + USDQ: 1000.0 + USDT: 1000.0 + WETH: 10.0 + +color: + top_pane: '#000000' + bottom_pane: '#000000' + output_pane: '#262626' + input_pane: '#1C1C1C' + logs_pane: '#121212' + terminal_primary: '#5FFFD7' + primary_label: '#5FFFD7' + secondary_label: '#FFFFFF' + success_label: '#5FFFD7' + warning_label: '#FFFF00' + info_label: '#5FD7FF' + error_label: '#FF0000' + gold_label: '#FFD700' + silver_label: '#C0C0C0' + bronze_label: '#CD7F32' + +# The tick size is the frequency with which the clock notifies the time iterators by calling the +# c_tick() method, that means for example that if the tick size is 1, the logic of the strategy +# will run every second. +tick_size: 1.0 + +market_data_collection: + market_data_collection_enabled: false + market_data_collection_interval: 60 + market_data_collection_depth: 20 diff --git a/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/conf_fee_overrides.yml b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/conf_fee_overrides.yml new file mode 100644 index 00000000..8dc9d57e --- /dev/null +++ b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/conf_fee_overrides.yml @@ -0,0 +1,526 @@ +######################################## +### Fee overrides configurations ### +######################################## + +# For more detailed information: https://docs.hummingbot.io +template_version: 14 + +# Example of the fields that can be specified to override the `TradeFeeFactory` default settings. +# If the field is missing or the value is left blank, the default value will be used. +# The percentage values are specified as 0.1 for 0.1%. +# +# [exchange name]_percent_fee_token: +# [exchange name]_maker_percent_fee: +# [exchange name]_taker_percent_fee: +# [exchange name]_buy_percent_fee_deducted_from_returns: # if False, the buy fee is added to the order costs +# [exchange name]_maker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]]) +# [exchange name]_taker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]]) + +binance_percent_fee_token: # BNB +binance_maker_percent_fee: # 0.75 +binance_taker_percent_fee: # 0.75 +binance_buy_percent_fee_deducted_from_returns: # True + +# List of supported Exchanges for which the user's conf/conf_fee_override.yml +# will work. This file currently needs to be in sync with hummingbot list of +# supported exchanges +ascend_ex_buy_percent_fee_deducted_from_returns: +ascend_ex_maker_fixed_fees: +ascend_ex_maker_percent_fee: +ascend_ex_percent_fee_token: +ascend_ex_taker_fixed_fees: +ascend_ex_taker_percent_fee: +binance_maker_fixed_fees: +binance_perpetual_buy_percent_fee_deducted_from_returns: +binance_perpetual_maker_fixed_fees: +binance_perpetual_maker_percent_fee: +binance_perpetual_percent_fee_token: +binance_perpetual_taker_fixed_fees: +binance_perpetual_taker_percent_fee: +binance_perpetual_testnet_buy_percent_fee_deducted_from_returns: +binance_perpetual_testnet_maker_fixed_fees: +binance_perpetual_testnet_maker_percent_fee: +binance_perpetual_testnet_percent_fee_token: +binance_perpetual_testnet_taker_fixed_fees: +binance_perpetual_testnet_taker_percent_fee: +binance_taker_fixed_fees: +binance_us_buy_percent_fee_deducted_from_returns: +binance_us_maker_fixed_fees: +binance_us_maker_percent_fee: +binance_us_percent_fee_token: +binance_us_taker_fixed_fees: +binance_us_taker_percent_fee: +bitfinex_buy_percent_fee_deducted_from_returns: +bitfinex_maker_fixed_fees: +bitfinex_maker_percent_fee: +bitfinex_percent_fee_token: +bitfinex_taker_fixed_fees: +bitfinex_taker_percent_fee: +bitmart_buy_percent_fee_deducted_from_returns: +bitmart_maker_fixed_fees: +bitmart_maker_percent_fee: +bitmart_percent_fee_token: +bitmart_taker_fixed_fees: +bitmart_taker_percent_fee: +btc_markets_percent_fee_token: +btc_markets_maker_percent_fee: +btc_markets_taker_percent_fee: +btc_markets_buy_percent_fee_deducted_from_returns: +bybit_perpetual_buy_percent_fee_deducted_from_returns: +bybit_perpetual_maker_fixed_fees: +bybit_perpetual_maker_percent_fee: +bybit_perpetual_percent_fee_token: +bybit_perpetual_taker_fixed_fees: +bybit_perpetual_taker_percent_fee: +bybit_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bybit_perpetual_testnet_maker_fixed_fees: +bybit_perpetual_testnet_maker_percent_fee: +bybit_perpetual_testnet_percent_fee_token: +bybit_perpetual_testnet_taker_fixed_fees: +bybit_perpetual_testnet_taker_percent_fee: +coinbase_pro_buy_percent_fee_deducted_from_returns: +coinbase_pro_maker_fixed_fees: +coinbase_pro_maker_percent_fee: +coinbase_pro_percent_fee_token: +coinbase_pro_taker_fixed_fees: +coinbase_pro_taker_percent_fee: +dydx_perpetual_buy_percent_fee_deducted_from_returns: +dydx_perpetual_maker_fixed_fees: +dydx_perpetual_maker_percent_fee: +dydx_perpetual_percent_fee_token: +dydx_perpetual_taker_fixed_fees: +dydx_perpetual_taker_percent_fee: +gate_io_buy_percent_fee_deducted_from_returns: +gate_io_maker_fixed_fees: +gate_io_maker_percent_fee: +gate_io_percent_fee_token: +gate_io_taker_fixed_fees: +gate_io_taker_percent_fee: +hitbtc_buy_percent_fee_deducted_from_returns: +hitbtc_maker_fixed_fees: +hitbtc_maker_percent_fee: +hitbtc_percent_fee_token: +hitbtc_taker_fixed_fees: +hitbtc_taker_percent_fee: +huobi_buy_percent_fee_deducted_from_returns: +huobi_maker_fixed_fees: +huobi_maker_percent_fee: +huobi_percent_fee_token: +huobi_taker_fixed_fees: +huobi_taker_percent_fee: +kraken_buy_percent_fee_deducted_from_returns: +kraken_maker_fixed_fees: +kraken_maker_percent_fee: +kraken_percent_fee_token: +kraken_taker_fixed_fees: +kraken_taker_percent_fee: +kucoin_buy_percent_fee_deducted_from_returns: +kucoin_maker_fixed_fees: +kucoin_maker_percent_fee: +kucoin_percent_fee_token: +kucoin_taker_fixed_fees: +kucoin_taker_percent_fee: +mexc_buy_percent_fee_deducted_from_returns: +mexc_maker_fixed_fees: +mexc_maker_percent_fee: +mexc_percent_fee_token: +mexc_taker_fixed_fees: +mexc_taker_percent_fee: +ndax_buy_percent_fee_deducted_from_returns: +ndax_maker_fixed_fees: +ndax_maker_percent_fee: +ndax_percent_fee_token: +ndax_taker_fixed_fees: +ndax_taker_percent_fee: +ndax_testnet_buy_percent_fee_deducted_from_returns: +ndax_testnet_maker_fixed_fees: +ndax_testnet_maker_percent_fee: +ndax_testnet_percent_fee_token: +ndax_testnet_taker_fixed_fees: +ndax_testnet_taker_percent_fee: +okx_buy_percent_fee_deducted_from_returns: +okx_maker_fixed_fees: +okx_maker_percent_fee: +okx_percent_fee_token: +okx_taker_fixed_fees: +okx_taker_percent_fee: +phemex_perpetual_percent_fee_token: +phemex_perpetual_maker_percent_fee: +phemex_perpetual_taker_percent_fee: +phemex_perpetual_buy_percent_fee_deducted_from_returns: +phemex_perpetual_maker_fixed_fees: +phemex_perpetual_taker_fixed_fees: +phemex_perpetual_testnet_percent_fee_token: +phemex_perpetual_testnet_maker_percent_fee: +phemex_perpetual_testnet_taker_percent_fee: +phemex_perpetual_testnet_buy_percent_fee_deducted_from_returns: +phemex_perpetual_testnet_maker_fixed_fees: +phemex_perpetual_testnet_taker_fixed_fees: +injective_v2_perpetual_percent_fee_token: +injective_v2_perpetual_maker_percent_fee: +injective_v2_perpetual_taker_percent_fee: +injective_v2_perpetual_buy_percent_fee_deducted_from_returns: +injective_v2_perpetual_maker_fixed_fees: +injective_v2_perpetual_taker_fixed_fees: +bitget_perpetual_percent_fee_token: +bitget_perpetual_maker_percent_fee: +bitget_perpetual_taker_percent_fee: +bitget_perpetual_buy_percent_fee_deducted_from_returns: +bitget_perpetual_maker_fixed_fees: +bitget_perpetual_taker_fixed_fees: +bit_com_perpetual_percent_fee_token: +bit_com_perpetual_maker_percent_fee: +bit_com_perpetual_taker_percent_fee: +bit_com_perpetual_buy_percent_fee_deducted_from_returns: +bit_com_perpetual_maker_fixed_fees: +bit_com_perpetual_taker_fixed_fees: +bit_com_perpetual_testnet_percent_fee_token: +bit_com_perpetual_testnet_maker_percent_fee: +bit_com_perpetual_testnet_taker_percent_fee: +bit_com_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bit_com_perpetual_testnet_maker_fixed_fees: +bit_com_perpetual_testnet_taker_fixed_fees: +gate_io_perpetual_percent_fee_token: +gate_io_perpetual_maker_percent_fee: +gate_io_perpetual_taker_percent_fee: +gate_io_perpetual_buy_percent_fee_deducted_from_returns: +gate_io_perpetual_maker_fixed_fees: +gate_io_perpetual_taker_fixed_fees: +bitmex_perpetual_percent_fee_token: +bitmex_perpetual_maker_percent_fee: +bitmex_perpetual_taker_percent_fee: +bitmex_perpetual_buy_percent_fee_deducted_from_returns: +bitmex_perpetual_maker_fixed_fees: +bitmex_perpetual_taker_fixed_fees: +bitmex_perpetual_testnet_percent_fee_token: +bitmex_perpetual_testnet_maker_percent_fee: +bitmex_perpetual_testnet_taker_percent_fee: +bitmex_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bitmex_perpetual_testnet_maker_fixed_fees: +bitmex_perpetual_testnet_taker_fixed_fees: +kucoin_perpetual_percent_fee_token: +kucoin_perpetual_maker_percent_fee: +kucoin_perpetual_taker_percent_fee: +kucoin_perpetual_buy_percent_fee_deducted_from_returns: +kucoin_perpetual_maker_fixed_fees: +kucoin_perpetual_taker_fixed_fees: +bitmex_percent_fee_token: +bitmex_maker_percent_fee: +bitmex_taker_percent_fee: +bitmex_buy_percent_fee_deducted_from_returns: +bitmex_maker_fixed_fees: +bitmex_taker_fixed_fees: +bitmex_testnet_percent_fee_token: +bitmex_testnet_maker_percent_fee: +bitmex_testnet_taker_percent_fee: +bitmex_testnet_buy_percent_fee_deducted_from_returns: +bitmex_testnet_maker_fixed_fees: +bitmex_testnet_taker_fixed_fees: +btc_markets_maker_fixed_fees: +btc_markets_taker_fixed_fees: +woo_x_percent_fee_token: +woo_x_maker_percent_fee: +woo_x_taker_percent_fee: +woo_x_buy_percent_fee_deducted_from_returns: +woo_x_maker_fixed_fees: +woo_x_taker_fixed_fees: +woo_x_testnet_percent_fee_token: +woo_x_testnet_maker_percent_fee: +woo_x_testnet_taker_percent_fee: +woo_x_testnet_buy_percent_fee_deducted_from_returns: +woo_x_testnet_maker_fixed_fees: +woo_x_testnet_taker_fixed_fees: +polkadex_percent_fee_token: +polkadex_maker_percent_fee: +polkadex_taker_percent_fee: +polkadex_buy_percent_fee_deducted_from_returns: +polkadex_maker_fixed_fees: +polkadex_taker_fixed_fees: +bybit_percent_fee_token: +bybit_maker_percent_fee: +bybit_taker_percent_fee: +bybit_buy_percent_fee_deducted_from_returns: +bybit_maker_fixed_fees: +bybit_taker_fixed_fees: +bybit_testnet_percent_fee_token: +bybit_testnet_maker_percent_fee: +bybit_testnet_taker_percent_fee: +bybit_testnet_buy_percent_fee_deducted_from_returns: +bybit_testnet_maker_fixed_fees: +bybit_testnet_taker_fixed_fees: +injective_v2_percent_fee_token: +injective_v2_maker_percent_fee: +injective_v2_taker_percent_fee: +injective_v2_buy_percent_fee_deducted_from_returns: +injective_v2_maker_fixed_fees: +injective_v2_taker_fixed_fees: +vertex_percent_fee_token: +vertex_maker_percent_fee: +vertex_taker_percent_fee: +vertex_buy_percent_fee_deducted_from_returns: +vertex_maker_fixed_fees: +vertex_taker_fixed_fees: +vertex_testnet_percent_fee_token: +vertex_testnet_maker_percent_fee: +vertex_testnet_taker_percent_fee: +vertex_testnet_buy_percent_fee_deducted_from_returns: +vertex_testnet_maker_fixed_fees: +vertex_testnet_taker_fixed_fees: +mock_paper_exchange_percent_fee_token: +mock_paper_exchange_maker_percent_fee: +mock_paper_exchange_taker_percent_fee: +mock_paper_exchange_buy_percent_fee_deducted_from_returns: +mock_paper_exchange_maker_fixed_fees: +mock_paper_exchange_taker_fixed_fees: +kucoin_perpetual_testnet_percent_fee_token: +kucoin_perpetual_testnet_maker_percent_fee: +kucoin_perpetual_testnet_taker_percent_fee: +kucoin_perpetual_testnet_buy_percent_fee_deducted_from_returns: +kucoin_perpetual_testnet_maker_fixed_fees: +kucoin_perpetual_testnet_taker_fixed_fees: +foxbit_percent_fee_token: +foxbit_maker_percent_fee: +foxbit_taker_percent_fee: +foxbit_buy_percent_fee_deducted_from_returns: +foxbit_maker_fixed_fees: +foxbit_taker_fixed_fees: +hyperliquid_perpetual_percent_fee_token: +hyperliquid_perpetual_maker_percent_fee: +hyperliquid_perpetual_taker_percent_fee: +hyperliquid_perpetual_buy_percent_fee_deducted_from_returns: +hyperliquid_perpetual_maker_fixed_fees: +hyperliquid_perpetual_taker_fixed_fees: +hyperliquid_perpetual_testnet_percent_fee_token: +hyperliquid_perpetual_testnet_maker_percent_fee: +hyperliquid_perpetual_testnet_taker_percent_fee: +hyperliquid_perpetual_testnet_buy_percent_fee_deducted_from_returns: +hyperliquid_perpetual_testnet_maker_fixed_fees: +hyperliquid_perpetual_testnet_taker_fixed_fees: +bitmart_perpetual_percent_fee_token: +bitmart_perpetual_maker_percent_fee: +bitmart_perpetual_taker_percent_fee: +bitmart_perpetual_buy_percent_fee_deducted_from_returns: +bitmart_perpetual_maker_fixed_fees: +bitmart_perpetual_taker_fixed_fees: +architect_perpetual_percent_fee_token: +architect_perpetual_maker_percent_fee: +architect_perpetual_taker_percent_fee: +architect_perpetual_buy_percent_fee_deducted_from_returns: +architect_perpetual_maker_fixed_fees: +architect_perpetual_taker_fixed_fees: +architect_perpetual_sandbox_percent_fee_token: +architect_perpetual_sandbox_maker_percent_fee: +architect_perpetual_sandbox_taker_percent_fee: +architect_perpetual_sandbox_buy_percent_fee_deducted_from_returns: +architect_perpetual_sandbox_maker_fixed_fees: +architect_perpetual_sandbox_taker_fixed_fees: +pacifica_perpetual_percent_fee_token: +pacifica_perpetual_maker_percent_fee: +pacifica_perpetual_taker_percent_fee: +pacifica_perpetual_buy_percent_fee_deducted_from_returns: +pacifica_perpetual_maker_fixed_fees: +pacifica_perpetual_taker_fixed_fees: +pacifica_perpetual_testnet_percent_fee_token: +pacifica_perpetual_testnet_maker_percent_fee: +pacifica_perpetual_testnet_taker_percent_fee: +pacifica_perpetual_testnet_buy_percent_fee_deducted_from_returns: +pacifica_perpetual_testnet_maker_fixed_fees: +pacifica_perpetual_testnet_taker_fixed_fees: +lighter_perpetual_percent_fee_token: +lighter_perpetual_maker_percent_fee: +lighter_perpetual_taker_percent_fee: +lighter_perpetual_buy_percent_fee_deducted_from_returns: +lighter_perpetual_maker_fixed_fees: +lighter_perpetual_taker_fixed_fees: +lighter_perpetual_testnet_percent_fee_token: +lighter_perpetual_testnet_maker_percent_fee: +lighter_perpetual_testnet_taker_percent_fee: +lighter_perpetual_testnet_buy_percent_fee_deducted_from_returns: +lighter_perpetual_testnet_maker_fixed_fees: +lighter_perpetual_testnet_taker_fixed_fees: +derive_perpetual_percent_fee_token: +derive_perpetual_maker_percent_fee: +derive_perpetual_taker_percent_fee: +derive_perpetual_buy_percent_fee_deducted_from_returns: +derive_perpetual_maker_fixed_fees: +derive_perpetual_taker_fixed_fees: +derive_perpetual_testnet_percent_fee_token: +derive_perpetual_testnet_maker_percent_fee: +derive_perpetual_testnet_taker_percent_fee: +derive_perpetual_testnet_buy_percent_fee_deducted_from_returns: +derive_perpetual_testnet_maker_fixed_fees: +derive_perpetual_testnet_taker_fixed_fees: +aevo_perpetual_percent_fee_token: +aevo_perpetual_maker_percent_fee: +aevo_perpetual_taker_percent_fee: +aevo_perpetual_buy_percent_fee_deducted_from_returns: +aevo_perpetual_maker_fixed_fees: +aevo_perpetual_taker_fixed_fees: +aevo_perpetual_testnet_percent_fee_token: +aevo_perpetual_testnet_maker_percent_fee: +aevo_perpetual_testnet_taker_percent_fee: +aevo_perpetual_testnet_buy_percent_fee_deducted_from_returns: +aevo_perpetual_testnet_maker_fixed_fees: +aevo_perpetual_testnet_taker_fixed_fees: +dydx_v4_perpetual_percent_fee_token: +dydx_v4_perpetual_maker_percent_fee: +dydx_v4_perpetual_taker_percent_fee: +dydx_v4_perpetual_buy_percent_fee_deducted_from_returns: +dydx_v4_perpetual_maker_fixed_fees: +dydx_v4_perpetual_taker_fixed_fees: +okx_perpetual_percent_fee_token: +okx_perpetual_maker_percent_fee: +okx_perpetual_taker_percent_fee: +okx_perpetual_buy_percent_fee_deducted_from_returns: +okx_perpetual_maker_fixed_fees: +okx_perpetual_taker_fixed_fees: +grvt_perpetual_percent_fee_token: +grvt_perpetual_maker_percent_fee: +grvt_perpetual_taker_percent_fee: +grvt_perpetual_buy_percent_fee_deducted_from_returns: +grvt_perpetual_maker_fixed_fees: +grvt_perpetual_taker_fixed_fees: +grvt_perpetual_testnet_percent_fee_token: +grvt_perpetual_testnet_maker_percent_fee: +grvt_perpetual_testnet_taker_percent_fee: +grvt_perpetual_testnet_buy_percent_fee_deducted_from_returns: +grvt_perpetual_testnet_maker_fixed_fees: +grvt_perpetual_testnet_taker_fixed_fees: +evedex_perpetual_percent_fee_token: +evedex_perpetual_maker_percent_fee: +evedex_perpetual_taker_percent_fee: +evedex_perpetual_buy_percent_fee_deducted_from_returns: +evedex_perpetual_maker_fixed_fees: +evedex_perpetual_taker_fixed_fees: +backpack_perpetual_percent_fee_token: +backpack_perpetual_maker_percent_fee: +backpack_perpetual_taker_percent_fee: +backpack_perpetual_buy_percent_fee_deducted_from_returns: +backpack_perpetual_maker_fixed_fees: +backpack_perpetual_taker_fixed_fees: +decibel_perpetual_percent_fee_token: +decibel_perpetual_maker_percent_fee: +decibel_perpetual_taker_percent_fee: +decibel_perpetual_buy_percent_fee_deducted_from_returns: +decibel_perpetual_maker_fixed_fees: +decibel_perpetual_taker_fixed_fees: +decibel_perpetual_testnet_percent_fee_token: +decibel_perpetual_testnet_maker_percent_fee: +decibel_perpetual_testnet_taker_percent_fee: +decibel_perpetual_testnet_buy_percent_fee_deducted_from_returns: +decibel_perpetual_testnet_maker_fixed_fees: +decibel_perpetual_testnet_taker_fixed_fees: +derive_percent_fee_token: +derive_maker_percent_fee: +derive_taker_percent_fee: +derive_buy_percent_fee_deducted_from_returns: +derive_maker_fixed_fees: +derive_taker_fixed_fees: +derive_testnet_percent_fee_token: +derive_testnet_maker_percent_fee: +derive_testnet_taker_percent_fee: +derive_testnet_buy_percent_fee_deducted_from_returns: +derive_testnet_maker_fixed_fees: +derive_testnet_taker_fixed_fees: +bitrue_percent_fee_token: +bitrue_maker_percent_fee: +bitrue_taker_percent_fee: +bitrue_buy_percent_fee_deducted_from_returns: +bitrue_maker_fixed_fees: +bitrue_taker_fixed_fees: +lambdaplex_percent_fee_token: +lambdaplex_maker_percent_fee: +lambdaplex_taker_percent_fee: +lambdaplex_buy_percent_fee_deducted_from_returns: +lambdaplex_maker_fixed_fees: +lambdaplex_taker_fixed_fees: +dexalot_percent_fee_token: +dexalot_maker_percent_fee: +dexalot_taker_percent_fee: +dexalot_buy_percent_fee_deducted_from_returns: +dexalot_maker_fixed_fees: +dexalot_taker_fixed_fees: +dexalot_testnet_percent_fee_token: +dexalot_testnet_maker_percent_fee: +dexalot_testnet_taker_percent_fee: +dexalot_testnet_buy_percent_fee_deducted_from_returns: +dexalot_testnet_maker_fixed_fees: +dexalot_testnet_taker_fixed_fees: +bing_x_percent_fee_token: +bing_x_maker_percent_fee: +bing_x_taker_percent_fee: +bing_x_buy_percent_fee_deducted_from_returns: +bing_x_maker_fixed_fees: +bing_x_taker_fixed_fees: +kucoin_hft_percent_fee_token: +kucoin_hft_maker_percent_fee: +kucoin_hft_taker_percent_fee: +kucoin_hft_buy_percent_fee_deducted_from_returns: +kucoin_hft_maker_fixed_fees: +kucoin_hft_taker_fixed_fees: +bitstamp_percent_fee_token: +bitstamp_maker_percent_fee: +bitstamp_taker_percent_fee: +bitstamp_buy_percent_fee_deducted_from_returns: +bitstamp_maker_fixed_fees: +bitstamp_taker_fixed_fees: +gemini_percent_fee_token: +gemini_maker_percent_fee: +gemini_taker_percent_fee: +gemini_buy_percent_fee_deducted_from_returns: +gemini_maker_fixed_fees: +gemini_taker_fixed_fees: +backpack_percent_fee_token: +backpack_maker_percent_fee: +backpack_taker_percent_fee: +backpack_buy_percent_fee_deducted_from_returns: +backpack_maker_fixed_fees: +backpack_taker_fixed_fees: +htx_percent_fee_token: +htx_maker_percent_fee: +htx_taker_percent_fee: +htx_buy_percent_fee_deducted_from_returns: +htx_maker_fixed_fees: +htx_taker_fixed_fees: +lighter_percent_fee_token: +lighter_maker_percent_fee: +lighter_taker_percent_fee: +lighter_buy_percent_fee_deducted_from_returns: +lighter_maker_fixed_fees: +lighter_taker_fixed_fees: +lighter_testnet_percent_fee_token: +lighter_testnet_maker_percent_fee: +lighter_testnet_taker_percent_fee: +lighter_testnet_buy_percent_fee_deducted_from_returns: +lighter_testnet_maker_fixed_fees: +lighter_testnet_taker_fixed_fees: +hyperliquid_percent_fee_token: +hyperliquid_maker_percent_fee: +hyperliquid_taker_percent_fee: +hyperliquid_buy_percent_fee_deducted_from_returns: +hyperliquid_maker_fixed_fees: +hyperliquid_taker_fixed_fees: +hyperliquid_testnet_percent_fee_token: +hyperliquid_testnet_maker_percent_fee: +hyperliquid_testnet_taker_percent_fee: +hyperliquid_testnet_buy_percent_fee_deducted_from_returns: +hyperliquid_testnet_maker_fixed_fees: +hyperliquid_testnet_taker_fixed_fees: +coinbase_advanced_trade_percent_fee_token: +coinbase_advanced_trade_maker_percent_fee: +coinbase_advanced_trade_taker_percent_fee: +coinbase_advanced_trade_buy_percent_fee_deducted_from_returns: +coinbase_advanced_trade_maker_fixed_fees: +coinbase_advanced_trade_taker_fixed_fees: +xrpl_percent_fee_token: +xrpl_maker_percent_fee: +xrpl_taker_percent_fee: +xrpl_buy_percent_fee_deducted_from_returns: +xrpl_maker_fixed_fees: +xrpl_taker_fixed_fees: +bitget_percent_fee_token: +bitget_maker_percent_fee: +bitget_taker_percent_fee: +bitget_buy_percent_fee_deducted_from_returns: +bitget_maker_fixed_fees: +bitget_taker_fixed_fees: diff --git a/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/connectors/.dockerignore b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/connectors/.dockerignore new file mode 100644 index 00000000..ad30bfec --- /dev/null +++ b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/connectors/.dockerignore @@ -0,0 +1 @@ +*.yml \ No newline at end of file diff --git a/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/connectors/.gitignore b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/connectors/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/connectors/xrpl.yml b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/connectors/xrpl.yml new file mode 100644 index 00000000..69ba6c49 --- /dev/null +++ b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/connectors/xrpl.yml @@ -0,0 +1,20 @@ +####################### +### xrpl config ### +####################### + +connector: xrpl + +xrpl_secret_key: 7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20226638643562313934656635386162626362646132313563636232626235636438227d2c202263697068657274657874223a20223763373032376666636337643732366464616462346439613365366431333764303639333238333934633739363330613166373135633138663762366539222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20223364353731366534366339393762613430666163336534623935366461643761227d2c20226d6163223a202266623862393637326439356234323361393930366565343336386633626235623434666535383739383061636537653730383538626564343439376261306466227d2c202276657273696f6e223a20332c2022616c696173223a2022227d + +wss_node_urls: +- wss://alien-magical-sky.xrp-mainnet.quiknode.pro/cdeda141d5c67711f061b73b1c4bf70eb163c6a8/ + +custom_markets: + SOLO-XRP: + base: SOLO + quote: XRP + base_issuer: rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz + quote_issuer: '' + trading_pair_symbol: null + +max_request_per_minute: 60 diff --git a/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/controllers/001_pmm_xrpl_USDC-XRP.yml b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/controllers/001_pmm_xrpl_USDC-XRP.yml new file mode 100644 index 00000000..368f76c5 --- /dev/null +++ b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/controllers/001_pmm_xrpl_USDC-XRP.yml @@ -0,0 +1,37 @@ +buy_amounts_pct: 1,1 +buy_cooldown_time: 60 +buy_position_effectivization_time: 300 +buy_spreads: 0.003,0.007 +connector_name: xrpl +controller_name: pmm_mister +controller_type: generic +executor_refresh_time: 60 +global_pnl_reference: position +global_sl_activation_from: target_base +global_sl_enabled: true +global_stop_loss: 0.04 +global_take_profit: 0.03 +global_tp_activation_from: min_base +global_tp_enabled: true +id: 001_pmm_xrpl_USDC-XRP +leverage: 1 +max_active_executors_by_level: 2 +max_base_pct: 0.7 +min_base_pct: 0.4 +min_skew: 2 +open_order_type: 3 +portfolio_allocation: 0.1 +position_profit_protection: true +price_distance_tolerance: 0.001 +refresh_tolerance: 0.001 +sell_amounts_pct: 1,1 +sell_cooldown_time: 180 +sell_position_effectivization_time: 300 +sell_spreads: 0.005,0.010 +take_profit: 0.0015 +take_profit_order_type: 3 +target_base_pct: 0.55 +tick_mode: false +tolerance_scaling: 1.3 +total_amount_quote: 50 +trading_pair: USDC-XRP diff --git a/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/hummingbot_logs.yml b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/hummingbot_logs.yml new file mode 100755 index 00000000..8e65271c --- /dev/null +++ b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/hummingbot_logs.yml @@ -0,0 +1,83 @@ +--- +version: 1 +template_version: 12 + +formatters: + simple: + format: "%(asctime)s - %(process)d - %(name)s - %(levelname)s - %(message)s" + +handlers: + console: + class: hummingbot.logger.cli_handler.CLIHandler + level: DEBUG + formatter: simple + stream: ext://sys.stdout + console_warning: + class: hummingbot.logger.cli_handler.CLIHandler + level: WARNING + formatter: simple + stream: ext://sys.stdout + console_info: + class: hummingbot.logger.cli_handler.CLIHandler + level: INFO + formatter: simple + stream: ext://sys.stdout + file_handler: + class: logging.handlers.TimedRotatingFileHandler + level: DEBUG + formatter: simple + filename: $PROJECT_DIR/logs/logs_$STRATEGY_FILE_PATH.log + encoding: utf8 + when: "D" + interval: 1 + backupCount: 7 + "null": + class: logging.NullHandler + level: DEBUG + +loggers: + hummingbot.core.utils.eth_gas_station_lookup: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.logger.log_server_client: + level: WARNING + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.logger.reporting_proxy_handler: + level: WARNING + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.strategy: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.connector: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.client: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.core.event.event_reporter: + level: EVENT_LOG + propagate: false + handlers: [file_handler] + mqtt: false + conf: + level: NETWORK + handlers: ["null"] + propagate: false + mqtt: false + +root: + level: INFO + handlers: [console, file_handler] + mqtt: true diff --git a/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/scripts/pmm_USDC-XRP_20260807-20260807-145233.yml b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/scripts/pmm_USDC-XRP_20260807-20260807-145233.yml new file mode 100644 index 00000000..67c21f58 --- /dev/null +++ b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/conf/scripts/pmm_USDC-XRP_20260807-20260807-145233.yml @@ -0,0 +1,3 @@ +controllers_config: +- 001_pmm_xrpl_USDC-XRP.yml +script_file_name: v2_with_controllers.py diff --git a/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/data/pmm_USDC-XRP_20260807-20260807-145233.sqlite b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/data/pmm_USDC-XRP_20260807-20260807-145233.sqlite new file mode 100644 index 00000000..f4a2c5bc Binary files /dev/null and b/bots/archived/pmm_USDC-XRP_20260807-20260807-145233/data/pmm_USDC-XRP_20260807-20260807-145233.sqlite differ diff --git a/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/.password_verification b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/.password_verification new file mode 100644 index 00000000..d1a274b6 --- /dev/null +++ b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/.password_verification @@ -0,0 +1 @@ +7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20226463613937613132666365383432623664323437613830393966313638373662227d2c202263697068657274657874223a20223537393331373032636666343037396439623430222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226536376664366338373033386336613439653066373938316366656138656539227d2c20226d6163223a202233366134323163646334303464393433343264656166613037623235373866363734383930653961393765303037393231333132326532333464303666323034227d2c202276657273696f6e223a20332c2022616c696173223a2022227d \ No newline at end of file diff --git a/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/conf_client.yml b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/conf_client.yml new file mode 100644 index 00000000..8d905b52 --- /dev/null +++ b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/conf_client.yml @@ -0,0 +1,179 @@ +#################################### +### client_config_map config ### +#################################### + +instance_id: sndk-gate-hft-mm-20260818-030708 + +# Fetch trading pairs from all exchanges if True, otherwise fetch only from connected exchanges. +fetch_pairs_from_all_exchanges: false + +log_level: INFO + +debug_console: false + +strategy_report_interval: 900.0 + +logger_override_whitelist: +- hummingbot.strategy.arbitrage +- hummingbot.strategy.cross_exchange_market_making +- conf + +log_file_path: /home/hummingbot/logs + +kill_switch_mode: {} + +# What to auto-fill in the prompt after each import command (start/config) +autofill_import: disabled + +# MQTT Bridge configuration. +mqtt_bridge: + mqtt_host: localhost + mqtt_port: 1883 + mqtt_username: '' + mqtt_password: '' + mqtt_namespace: hbot + mqtt_ssl: false + mqtt_logger: true + mqtt_notifier: true + mqtt_commands: true + mqtt_events: true + mqtt_external_events: true + mqtt_autostart: true + +# Error log sharing +send_error_logs: true + +# Advanced database options, currently supports SQLAlchemy's included dialects +# Reference: https://docs.sqlalchemy.org/en/13/dialects/ +# To use an instance of SQLite DB the required configuration is +# db_engine: sqlite +# To use a DBMS the required configuration is +# db_host: 127.0.0.1 +# db_port: 3306 +# db_username: username +# db_password: password +# db_name: dbname +db_mode: + db_engine: sqlite + +# Balance Limit Configurations +# e.g. Setting USDT and BTC limits on Binance. +# balance_asset_limit: +# binance: +# BTC: 0.1 +# USDT: 1000 +balance_asset_limit: + ascend_ex: {} + ascend_ex_paper_trade: {} + binance: {} + binance_paper_trade: {} + binance_us: {} + bitfinex: {} + bitmart: {} + bitmex: {} + bitmex_testnet: {} + btc_markets: {} + bybit: {} + bybit_testnet: {} + coinbase_pro: {} + foxbit: {} + gate_io: {} + gate_io_paper_trade: {} + hitbtc: {} + huobi: {} + injective_v2: {} + kraken: {} + kucoin: {} + kucoin_paper_trade: {} + mexc: {} + mock_paper_exchange: {} + ndax: {} + ndax_testnet: {} + okx: {} + polkadex: {} + vertex: {} + vertex_testnet: {} + woo_x: {} + woo_x_testnet: {} + +# Fixed gas price (in Gwei) for Ethereum transactions +manual_gas_price: 50.0 + +# Gateway API Configurations +# default host to only use localhost +# Port need to match the final installation port for Gateway +gateway: + gateway_api_host: localhost + gateway_api_port: '15888' + gateway_use_ssl: true + +# Whether to enable aggregated order and trade data collection +anonymized_metrics_mode: + anonymized_metrics_interval_min: 15.0 + +# A source for rate oracle, currently binance, coin_gecko, coin_cap, dexalot, evedex_perpetual, decibel_perpetual, kucoin, gate_io, backpack, coinbase_advanced_trade, hyperliquid, hyperliquid_perpetual, architect_perpetual, derive, mexc +rate_oracle_source: + name: gate_io + +# A universal token which to display tokens values in, e.g. USD,EUR,BTC +global_token: + global_token_name: USDT + global_token_symbol: $ + usd_equivalent_tokens: + - USD + +# Percentage of API rate limits (on any exchange and any end point) allocated to this bot instance. +# Enter 50 to indicate 50%. E.g. if the API rate limit is 100 calls per second, and you allocate +# 50% to this setting, the bot will have a maximum (limit) of 50 calls per second +rate_limits_share_pct: 100.0 + +commands_timeout: + create_command_timeout: 10.0 + other_commands_timeout: 30.0 + +# Tabulate table format style (https://github.com/astanin/python-tabulate#table-format) +tables_format: psql + +paper_trade: + paper_trade_exchanges: + - binance + - kucoin + - ascend_ex + - gate_io + paper_trade_account_balance: + BTC: 1.0 + DAI: 1000.0 + ETH: 10.0 + ONE: 1000.0 + TUSD: 1000.0 + USDC: 1000.0 + USDQ: 1000.0 + USDT: 1000.0 + WETH: 10.0 + +color: + top_pane: '#000000' + bottom_pane: '#000000' + output_pane: '#262626' + input_pane: '#1C1C1C' + logs_pane: '#121212' + terminal_primary: '#5FFFD7' + primary_label: '#5FFFD7' + secondary_label: '#FFFFFF' + success_label: '#5FFFD7' + warning_label: '#FFFF00' + info_label: '#5FD7FF' + error_label: '#FF0000' + gold_label: '#FFD700' + silver_label: '#C0C0C0' + bronze_label: '#CD7F32' + +# The tick size is the frequency with which the clock notifies the time iterators by calling the +# c_tick() method, that means for example that if the tick size is 1, the logic of the strategy +# will run every second. +tick_size: 1.0 + +market_data_collection: + market_data_collection_enabled: false + market_data_collection_interval: 60 + market_data_collection_depth: 20 diff --git a/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/conf_fee_overrides.yml b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/conf_fee_overrides.yml new file mode 100644 index 00000000..8dc9d57e --- /dev/null +++ b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/conf_fee_overrides.yml @@ -0,0 +1,526 @@ +######################################## +### Fee overrides configurations ### +######################################## + +# For more detailed information: https://docs.hummingbot.io +template_version: 14 + +# Example of the fields that can be specified to override the `TradeFeeFactory` default settings. +# If the field is missing or the value is left blank, the default value will be used. +# The percentage values are specified as 0.1 for 0.1%. +# +# [exchange name]_percent_fee_token: +# [exchange name]_maker_percent_fee: +# [exchange name]_taker_percent_fee: +# [exchange name]_buy_percent_fee_deducted_from_returns: # if False, the buy fee is added to the order costs +# [exchange name]_maker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]]) +# [exchange name]_taker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]]) + +binance_percent_fee_token: # BNB +binance_maker_percent_fee: # 0.75 +binance_taker_percent_fee: # 0.75 +binance_buy_percent_fee_deducted_from_returns: # True + +# List of supported Exchanges for which the user's conf/conf_fee_override.yml +# will work. This file currently needs to be in sync with hummingbot list of +# supported exchanges +ascend_ex_buy_percent_fee_deducted_from_returns: +ascend_ex_maker_fixed_fees: +ascend_ex_maker_percent_fee: +ascend_ex_percent_fee_token: +ascend_ex_taker_fixed_fees: +ascend_ex_taker_percent_fee: +binance_maker_fixed_fees: +binance_perpetual_buy_percent_fee_deducted_from_returns: +binance_perpetual_maker_fixed_fees: +binance_perpetual_maker_percent_fee: +binance_perpetual_percent_fee_token: +binance_perpetual_taker_fixed_fees: +binance_perpetual_taker_percent_fee: +binance_perpetual_testnet_buy_percent_fee_deducted_from_returns: +binance_perpetual_testnet_maker_fixed_fees: +binance_perpetual_testnet_maker_percent_fee: +binance_perpetual_testnet_percent_fee_token: +binance_perpetual_testnet_taker_fixed_fees: +binance_perpetual_testnet_taker_percent_fee: +binance_taker_fixed_fees: +binance_us_buy_percent_fee_deducted_from_returns: +binance_us_maker_fixed_fees: +binance_us_maker_percent_fee: +binance_us_percent_fee_token: +binance_us_taker_fixed_fees: +binance_us_taker_percent_fee: +bitfinex_buy_percent_fee_deducted_from_returns: +bitfinex_maker_fixed_fees: +bitfinex_maker_percent_fee: +bitfinex_percent_fee_token: +bitfinex_taker_fixed_fees: +bitfinex_taker_percent_fee: +bitmart_buy_percent_fee_deducted_from_returns: +bitmart_maker_fixed_fees: +bitmart_maker_percent_fee: +bitmart_percent_fee_token: +bitmart_taker_fixed_fees: +bitmart_taker_percent_fee: +btc_markets_percent_fee_token: +btc_markets_maker_percent_fee: +btc_markets_taker_percent_fee: +btc_markets_buy_percent_fee_deducted_from_returns: +bybit_perpetual_buy_percent_fee_deducted_from_returns: +bybit_perpetual_maker_fixed_fees: +bybit_perpetual_maker_percent_fee: +bybit_perpetual_percent_fee_token: +bybit_perpetual_taker_fixed_fees: +bybit_perpetual_taker_percent_fee: +bybit_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bybit_perpetual_testnet_maker_fixed_fees: +bybit_perpetual_testnet_maker_percent_fee: +bybit_perpetual_testnet_percent_fee_token: +bybit_perpetual_testnet_taker_fixed_fees: +bybit_perpetual_testnet_taker_percent_fee: +coinbase_pro_buy_percent_fee_deducted_from_returns: +coinbase_pro_maker_fixed_fees: +coinbase_pro_maker_percent_fee: +coinbase_pro_percent_fee_token: +coinbase_pro_taker_fixed_fees: +coinbase_pro_taker_percent_fee: +dydx_perpetual_buy_percent_fee_deducted_from_returns: +dydx_perpetual_maker_fixed_fees: +dydx_perpetual_maker_percent_fee: +dydx_perpetual_percent_fee_token: +dydx_perpetual_taker_fixed_fees: +dydx_perpetual_taker_percent_fee: +gate_io_buy_percent_fee_deducted_from_returns: +gate_io_maker_fixed_fees: +gate_io_maker_percent_fee: +gate_io_percent_fee_token: +gate_io_taker_fixed_fees: +gate_io_taker_percent_fee: +hitbtc_buy_percent_fee_deducted_from_returns: +hitbtc_maker_fixed_fees: +hitbtc_maker_percent_fee: +hitbtc_percent_fee_token: +hitbtc_taker_fixed_fees: +hitbtc_taker_percent_fee: +huobi_buy_percent_fee_deducted_from_returns: +huobi_maker_fixed_fees: +huobi_maker_percent_fee: +huobi_percent_fee_token: +huobi_taker_fixed_fees: +huobi_taker_percent_fee: +kraken_buy_percent_fee_deducted_from_returns: +kraken_maker_fixed_fees: +kraken_maker_percent_fee: +kraken_percent_fee_token: +kraken_taker_fixed_fees: +kraken_taker_percent_fee: +kucoin_buy_percent_fee_deducted_from_returns: +kucoin_maker_fixed_fees: +kucoin_maker_percent_fee: +kucoin_percent_fee_token: +kucoin_taker_fixed_fees: +kucoin_taker_percent_fee: +mexc_buy_percent_fee_deducted_from_returns: +mexc_maker_fixed_fees: +mexc_maker_percent_fee: +mexc_percent_fee_token: +mexc_taker_fixed_fees: +mexc_taker_percent_fee: +ndax_buy_percent_fee_deducted_from_returns: +ndax_maker_fixed_fees: +ndax_maker_percent_fee: +ndax_percent_fee_token: +ndax_taker_fixed_fees: +ndax_taker_percent_fee: +ndax_testnet_buy_percent_fee_deducted_from_returns: +ndax_testnet_maker_fixed_fees: +ndax_testnet_maker_percent_fee: +ndax_testnet_percent_fee_token: +ndax_testnet_taker_fixed_fees: +ndax_testnet_taker_percent_fee: +okx_buy_percent_fee_deducted_from_returns: +okx_maker_fixed_fees: +okx_maker_percent_fee: +okx_percent_fee_token: +okx_taker_fixed_fees: +okx_taker_percent_fee: +phemex_perpetual_percent_fee_token: +phemex_perpetual_maker_percent_fee: +phemex_perpetual_taker_percent_fee: +phemex_perpetual_buy_percent_fee_deducted_from_returns: +phemex_perpetual_maker_fixed_fees: +phemex_perpetual_taker_fixed_fees: +phemex_perpetual_testnet_percent_fee_token: +phemex_perpetual_testnet_maker_percent_fee: +phemex_perpetual_testnet_taker_percent_fee: +phemex_perpetual_testnet_buy_percent_fee_deducted_from_returns: +phemex_perpetual_testnet_maker_fixed_fees: +phemex_perpetual_testnet_taker_fixed_fees: +injective_v2_perpetual_percent_fee_token: +injective_v2_perpetual_maker_percent_fee: +injective_v2_perpetual_taker_percent_fee: +injective_v2_perpetual_buy_percent_fee_deducted_from_returns: +injective_v2_perpetual_maker_fixed_fees: +injective_v2_perpetual_taker_fixed_fees: +bitget_perpetual_percent_fee_token: +bitget_perpetual_maker_percent_fee: +bitget_perpetual_taker_percent_fee: +bitget_perpetual_buy_percent_fee_deducted_from_returns: +bitget_perpetual_maker_fixed_fees: +bitget_perpetual_taker_fixed_fees: +bit_com_perpetual_percent_fee_token: +bit_com_perpetual_maker_percent_fee: +bit_com_perpetual_taker_percent_fee: +bit_com_perpetual_buy_percent_fee_deducted_from_returns: +bit_com_perpetual_maker_fixed_fees: +bit_com_perpetual_taker_fixed_fees: +bit_com_perpetual_testnet_percent_fee_token: +bit_com_perpetual_testnet_maker_percent_fee: +bit_com_perpetual_testnet_taker_percent_fee: +bit_com_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bit_com_perpetual_testnet_maker_fixed_fees: +bit_com_perpetual_testnet_taker_fixed_fees: +gate_io_perpetual_percent_fee_token: +gate_io_perpetual_maker_percent_fee: +gate_io_perpetual_taker_percent_fee: +gate_io_perpetual_buy_percent_fee_deducted_from_returns: +gate_io_perpetual_maker_fixed_fees: +gate_io_perpetual_taker_fixed_fees: +bitmex_perpetual_percent_fee_token: +bitmex_perpetual_maker_percent_fee: +bitmex_perpetual_taker_percent_fee: +bitmex_perpetual_buy_percent_fee_deducted_from_returns: +bitmex_perpetual_maker_fixed_fees: +bitmex_perpetual_taker_fixed_fees: +bitmex_perpetual_testnet_percent_fee_token: +bitmex_perpetual_testnet_maker_percent_fee: +bitmex_perpetual_testnet_taker_percent_fee: +bitmex_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bitmex_perpetual_testnet_maker_fixed_fees: +bitmex_perpetual_testnet_taker_fixed_fees: +kucoin_perpetual_percent_fee_token: +kucoin_perpetual_maker_percent_fee: +kucoin_perpetual_taker_percent_fee: +kucoin_perpetual_buy_percent_fee_deducted_from_returns: +kucoin_perpetual_maker_fixed_fees: +kucoin_perpetual_taker_fixed_fees: +bitmex_percent_fee_token: +bitmex_maker_percent_fee: +bitmex_taker_percent_fee: +bitmex_buy_percent_fee_deducted_from_returns: +bitmex_maker_fixed_fees: +bitmex_taker_fixed_fees: +bitmex_testnet_percent_fee_token: +bitmex_testnet_maker_percent_fee: +bitmex_testnet_taker_percent_fee: +bitmex_testnet_buy_percent_fee_deducted_from_returns: +bitmex_testnet_maker_fixed_fees: +bitmex_testnet_taker_fixed_fees: +btc_markets_maker_fixed_fees: +btc_markets_taker_fixed_fees: +woo_x_percent_fee_token: +woo_x_maker_percent_fee: +woo_x_taker_percent_fee: +woo_x_buy_percent_fee_deducted_from_returns: +woo_x_maker_fixed_fees: +woo_x_taker_fixed_fees: +woo_x_testnet_percent_fee_token: +woo_x_testnet_maker_percent_fee: +woo_x_testnet_taker_percent_fee: +woo_x_testnet_buy_percent_fee_deducted_from_returns: +woo_x_testnet_maker_fixed_fees: +woo_x_testnet_taker_fixed_fees: +polkadex_percent_fee_token: +polkadex_maker_percent_fee: +polkadex_taker_percent_fee: +polkadex_buy_percent_fee_deducted_from_returns: +polkadex_maker_fixed_fees: +polkadex_taker_fixed_fees: +bybit_percent_fee_token: +bybit_maker_percent_fee: +bybit_taker_percent_fee: +bybit_buy_percent_fee_deducted_from_returns: +bybit_maker_fixed_fees: +bybit_taker_fixed_fees: +bybit_testnet_percent_fee_token: +bybit_testnet_maker_percent_fee: +bybit_testnet_taker_percent_fee: +bybit_testnet_buy_percent_fee_deducted_from_returns: +bybit_testnet_maker_fixed_fees: +bybit_testnet_taker_fixed_fees: +injective_v2_percent_fee_token: +injective_v2_maker_percent_fee: +injective_v2_taker_percent_fee: +injective_v2_buy_percent_fee_deducted_from_returns: +injective_v2_maker_fixed_fees: +injective_v2_taker_fixed_fees: +vertex_percent_fee_token: +vertex_maker_percent_fee: +vertex_taker_percent_fee: +vertex_buy_percent_fee_deducted_from_returns: +vertex_maker_fixed_fees: +vertex_taker_fixed_fees: +vertex_testnet_percent_fee_token: +vertex_testnet_maker_percent_fee: +vertex_testnet_taker_percent_fee: +vertex_testnet_buy_percent_fee_deducted_from_returns: +vertex_testnet_maker_fixed_fees: +vertex_testnet_taker_fixed_fees: +mock_paper_exchange_percent_fee_token: +mock_paper_exchange_maker_percent_fee: +mock_paper_exchange_taker_percent_fee: +mock_paper_exchange_buy_percent_fee_deducted_from_returns: +mock_paper_exchange_maker_fixed_fees: +mock_paper_exchange_taker_fixed_fees: +kucoin_perpetual_testnet_percent_fee_token: +kucoin_perpetual_testnet_maker_percent_fee: +kucoin_perpetual_testnet_taker_percent_fee: +kucoin_perpetual_testnet_buy_percent_fee_deducted_from_returns: +kucoin_perpetual_testnet_maker_fixed_fees: +kucoin_perpetual_testnet_taker_fixed_fees: +foxbit_percent_fee_token: +foxbit_maker_percent_fee: +foxbit_taker_percent_fee: +foxbit_buy_percent_fee_deducted_from_returns: +foxbit_maker_fixed_fees: +foxbit_taker_fixed_fees: +hyperliquid_perpetual_percent_fee_token: +hyperliquid_perpetual_maker_percent_fee: +hyperliquid_perpetual_taker_percent_fee: +hyperliquid_perpetual_buy_percent_fee_deducted_from_returns: +hyperliquid_perpetual_maker_fixed_fees: +hyperliquid_perpetual_taker_fixed_fees: +hyperliquid_perpetual_testnet_percent_fee_token: +hyperliquid_perpetual_testnet_maker_percent_fee: +hyperliquid_perpetual_testnet_taker_percent_fee: +hyperliquid_perpetual_testnet_buy_percent_fee_deducted_from_returns: +hyperliquid_perpetual_testnet_maker_fixed_fees: +hyperliquid_perpetual_testnet_taker_fixed_fees: +bitmart_perpetual_percent_fee_token: +bitmart_perpetual_maker_percent_fee: +bitmart_perpetual_taker_percent_fee: +bitmart_perpetual_buy_percent_fee_deducted_from_returns: +bitmart_perpetual_maker_fixed_fees: +bitmart_perpetual_taker_fixed_fees: +architect_perpetual_percent_fee_token: +architect_perpetual_maker_percent_fee: +architect_perpetual_taker_percent_fee: +architect_perpetual_buy_percent_fee_deducted_from_returns: +architect_perpetual_maker_fixed_fees: +architect_perpetual_taker_fixed_fees: +architect_perpetual_sandbox_percent_fee_token: +architect_perpetual_sandbox_maker_percent_fee: +architect_perpetual_sandbox_taker_percent_fee: +architect_perpetual_sandbox_buy_percent_fee_deducted_from_returns: +architect_perpetual_sandbox_maker_fixed_fees: +architect_perpetual_sandbox_taker_fixed_fees: +pacifica_perpetual_percent_fee_token: +pacifica_perpetual_maker_percent_fee: +pacifica_perpetual_taker_percent_fee: +pacifica_perpetual_buy_percent_fee_deducted_from_returns: +pacifica_perpetual_maker_fixed_fees: +pacifica_perpetual_taker_fixed_fees: +pacifica_perpetual_testnet_percent_fee_token: +pacifica_perpetual_testnet_maker_percent_fee: +pacifica_perpetual_testnet_taker_percent_fee: +pacifica_perpetual_testnet_buy_percent_fee_deducted_from_returns: +pacifica_perpetual_testnet_maker_fixed_fees: +pacifica_perpetual_testnet_taker_fixed_fees: +lighter_perpetual_percent_fee_token: +lighter_perpetual_maker_percent_fee: +lighter_perpetual_taker_percent_fee: +lighter_perpetual_buy_percent_fee_deducted_from_returns: +lighter_perpetual_maker_fixed_fees: +lighter_perpetual_taker_fixed_fees: +lighter_perpetual_testnet_percent_fee_token: +lighter_perpetual_testnet_maker_percent_fee: +lighter_perpetual_testnet_taker_percent_fee: +lighter_perpetual_testnet_buy_percent_fee_deducted_from_returns: +lighter_perpetual_testnet_maker_fixed_fees: +lighter_perpetual_testnet_taker_fixed_fees: +derive_perpetual_percent_fee_token: +derive_perpetual_maker_percent_fee: +derive_perpetual_taker_percent_fee: +derive_perpetual_buy_percent_fee_deducted_from_returns: +derive_perpetual_maker_fixed_fees: +derive_perpetual_taker_fixed_fees: +derive_perpetual_testnet_percent_fee_token: +derive_perpetual_testnet_maker_percent_fee: +derive_perpetual_testnet_taker_percent_fee: +derive_perpetual_testnet_buy_percent_fee_deducted_from_returns: +derive_perpetual_testnet_maker_fixed_fees: +derive_perpetual_testnet_taker_fixed_fees: +aevo_perpetual_percent_fee_token: +aevo_perpetual_maker_percent_fee: +aevo_perpetual_taker_percent_fee: +aevo_perpetual_buy_percent_fee_deducted_from_returns: +aevo_perpetual_maker_fixed_fees: +aevo_perpetual_taker_fixed_fees: +aevo_perpetual_testnet_percent_fee_token: +aevo_perpetual_testnet_maker_percent_fee: +aevo_perpetual_testnet_taker_percent_fee: +aevo_perpetual_testnet_buy_percent_fee_deducted_from_returns: +aevo_perpetual_testnet_maker_fixed_fees: +aevo_perpetual_testnet_taker_fixed_fees: +dydx_v4_perpetual_percent_fee_token: +dydx_v4_perpetual_maker_percent_fee: +dydx_v4_perpetual_taker_percent_fee: +dydx_v4_perpetual_buy_percent_fee_deducted_from_returns: +dydx_v4_perpetual_maker_fixed_fees: +dydx_v4_perpetual_taker_fixed_fees: +okx_perpetual_percent_fee_token: +okx_perpetual_maker_percent_fee: +okx_perpetual_taker_percent_fee: +okx_perpetual_buy_percent_fee_deducted_from_returns: +okx_perpetual_maker_fixed_fees: +okx_perpetual_taker_fixed_fees: +grvt_perpetual_percent_fee_token: +grvt_perpetual_maker_percent_fee: +grvt_perpetual_taker_percent_fee: +grvt_perpetual_buy_percent_fee_deducted_from_returns: +grvt_perpetual_maker_fixed_fees: +grvt_perpetual_taker_fixed_fees: +grvt_perpetual_testnet_percent_fee_token: +grvt_perpetual_testnet_maker_percent_fee: +grvt_perpetual_testnet_taker_percent_fee: +grvt_perpetual_testnet_buy_percent_fee_deducted_from_returns: +grvt_perpetual_testnet_maker_fixed_fees: +grvt_perpetual_testnet_taker_fixed_fees: +evedex_perpetual_percent_fee_token: +evedex_perpetual_maker_percent_fee: +evedex_perpetual_taker_percent_fee: +evedex_perpetual_buy_percent_fee_deducted_from_returns: +evedex_perpetual_maker_fixed_fees: +evedex_perpetual_taker_fixed_fees: +backpack_perpetual_percent_fee_token: +backpack_perpetual_maker_percent_fee: +backpack_perpetual_taker_percent_fee: +backpack_perpetual_buy_percent_fee_deducted_from_returns: +backpack_perpetual_maker_fixed_fees: +backpack_perpetual_taker_fixed_fees: +decibel_perpetual_percent_fee_token: +decibel_perpetual_maker_percent_fee: +decibel_perpetual_taker_percent_fee: +decibel_perpetual_buy_percent_fee_deducted_from_returns: +decibel_perpetual_maker_fixed_fees: +decibel_perpetual_taker_fixed_fees: +decibel_perpetual_testnet_percent_fee_token: +decibel_perpetual_testnet_maker_percent_fee: +decibel_perpetual_testnet_taker_percent_fee: +decibel_perpetual_testnet_buy_percent_fee_deducted_from_returns: +decibel_perpetual_testnet_maker_fixed_fees: +decibel_perpetual_testnet_taker_fixed_fees: +derive_percent_fee_token: +derive_maker_percent_fee: +derive_taker_percent_fee: +derive_buy_percent_fee_deducted_from_returns: +derive_maker_fixed_fees: +derive_taker_fixed_fees: +derive_testnet_percent_fee_token: +derive_testnet_maker_percent_fee: +derive_testnet_taker_percent_fee: +derive_testnet_buy_percent_fee_deducted_from_returns: +derive_testnet_maker_fixed_fees: +derive_testnet_taker_fixed_fees: +bitrue_percent_fee_token: +bitrue_maker_percent_fee: +bitrue_taker_percent_fee: +bitrue_buy_percent_fee_deducted_from_returns: +bitrue_maker_fixed_fees: +bitrue_taker_fixed_fees: +lambdaplex_percent_fee_token: +lambdaplex_maker_percent_fee: +lambdaplex_taker_percent_fee: +lambdaplex_buy_percent_fee_deducted_from_returns: +lambdaplex_maker_fixed_fees: +lambdaplex_taker_fixed_fees: +dexalot_percent_fee_token: +dexalot_maker_percent_fee: +dexalot_taker_percent_fee: +dexalot_buy_percent_fee_deducted_from_returns: +dexalot_maker_fixed_fees: +dexalot_taker_fixed_fees: +dexalot_testnet_percent_fee_token: +dexalot_testnet_maker_percent_fee: +dexalot_testnet_taker_percent_fee: +dexalot_testnet_buy_percent_fee_deducted_from_returns: +dexalot_testnet_maker_fixed_fees: +dexalot_testnet_taker_fixed_fees: +bing_x_percent_fee_token: +bing_x_maker_percent_fee: +bing_x_taker_percent_fee: +bing_x_buy_percent_fee_deducted_from_returns: +bing_x_maker_fixed_fees: +bing_x_taker_fixed_fees: +kucoin_hft_percent_fee_token: +kucoin_hft_maker_percent_fee: +kucoin_hft_taker_percent_fee: +kucoin_hft_buy_percent_fee_deducted_from_returns: +kucoin_hft_maker_fixed_fees: +kucoin_hft_taker_fixed_fees: +bitstamp_percent_fee_token: +bitstamp_maker_percent_fee: +bitstamp_taker_percent_fee: +bitstamp_buy_percent_fee_deducted_from_returns: +bitstamp_maker_fixed_fees: +bitstamp_taker_fixed_fees: +gemini_percent_fee_token: +gemini_maker_percent_fee: +gemini_taker_percent_fee: +gemini_buy_percent_fee_deducted_from_returns: +gemini_maker_fixed_fees: +gemini_taker_fixed_fees: +backpack_percent_fee_token: +backpack_maker_percent_fee: +backpack_taker_percent_fee: +backpack_buy_percent_fee_deducted_from_returns: +backpack_maker_fixed_fees: +backpack_taker_fixed_fees: +htx_percent_fee_token: +htx_maker_percent_fee: +htx_taker_percent_fee: +htx_buy_percent_fee_deducted_from_returns: +htx_maker_fixed_fees: +htx_taker_fixed_fees: +lighter_percent_fee_token: +lighter_maker_percent_fee: +lighter_taker_percent_fee: +lighter_buy_percent_fee_deducted_from_returns: +lighter_maker_fixed_fees: +lighter_taker_fixed_fees: +lighter_testnet_percent_fee_token: +lighter_testnet_maker_percent_fee: +lighter_testnet_taker_percent_fee: +lighter_testnet_buy_percent_fee_deducted_from_returns: +lighter_testnet_maker_fixed_fees: +lighter_testnet_taker_fixed_fees: +hyperliquid_percent_fee_token: +hyperliquid_maker_percent_fee: +hyperliquid_taker_percent_fee: +hyperliquid_buy_percent_fee_deducted_from_returns: +hyperliquid_maker_fixed_fees: +hyperliquid_taker_fixed_fees: +hyperliquid_testnet_percent_fee_token: +hyperliquid_testnet_maker_percent_fee: +hyperliquid_testnet_taker_percent_fee: +hyperliquid_testnet_buy_percent_fee_deducted_from_returns: +hyperliquid_testnet_maker_fixed_fees: +hyperliquid_testnet_taker_fixed_fees: +coinbase_advanced_trade_percent_fee_token: +coinbase_advanced_trade_maker_percent_fee: +coinbase_advanced_trade_taker_percent_fee: +coinbase_advanced_trade_buy_percent_fee_deducted_from_returns: +coinbase_advanced_trade_maker_fixed_fees: +coinbase_advanced_trade_taker_fixed_fees: +xrpl_percent_fee_token: +xrpl_maker_percent_fee: +xrpl_taker_percent_fee: +xrpl_buy_percent_fee_deducted_from_returns: +xrpl_maker_fixed_fees: +xrpl_taker_fixed_fees: +bitget_percent_fee_token: +bitget_maker_percent_fee: +bitget_taker_percent_fee: +bitget_buy_percent_fee_deducted_from_returns: +bitget_maker_fixed_fees: +bitget_taker_fixed_fees: diff --git a/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/connectors/.dockerignore b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/connectors/.dockerignore new file mode 100644 index 00000000..ad30bfec --- /dev/null +++ b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/connectors/.dockerignore @@ -0,0 +1 @@ +*.yml \ No newline at end of file diff --git a/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/connectors/.gitignore b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/connectors/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/connectors/gate_io_perpetual.yml b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/connectors/gate_io_perpetual.yml new file mode 100644 index 00000000..503e58d7 --- /dev/null +++ b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/connectors/gate_io_perpetual.yml @@ -0,0 +1,11 @@ +#################################### +### gate_io_perpetual config ### +#################################### + +connector: gate_io_perpetual + +gate_io_perpetual_api_key: 7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20223536666463386230626433613261353136633462393737313437353638653139227d2c202263697068657274657874223a202235376637663363336662626464646562386632666465316637643039306231393234656265393133643664353836653362636537316537643561363138613561222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20223731316464306439376336316430306331353461306362633663613833313364227d2c20226d6163223a202236303137366133306439313765376663623436623432316639376430356637326335343266363238633034383164646136653735643638653732623663613238227d2c202276657273696f6e223a20332c2022616c696173223a2022227d + +gate_io_perpetual_secret_key: 7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20223734303930346566636339636331613064313163393065346133646263616264227d2c202263697068657274657874223a20223665653731376164313439326139363463366539643036633662386132666661373532626636653831393965653963346333356239336663616562633738346335656235626231626166666365646662336361646633336164386630623935306466306163333763303934376165356236613761646238326466613134626333222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226332396631646230333065323564363734643663306365643166383161313462227d2c20226d6163223a202265376239346564316564303537646434366639356537646166653464326533386465326330353365636261356534613662636335623232313566393164393963227d2c202276657273696f6e223a20332c2022616c696173223a2022227d + +gate_io_perpetual_user_id: 7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20226165613865323531633839323037663132373166323739653233633835356261227d2c202263697068657274657874223a20226338663061646538666230353335222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226438303663636334663833366538636662613366323761396361643364623765227d2c20226d6163223a202230383430646330336463646636313263383433326262373330623466613133363333613136626562373637306533363031656265343838653461646636333634227d2c202276657273696f6e223a20332c2022616c696173223a2022227d diff --git a/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/controllers/sndk_gate_hft_mm.yml b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/controllers/sndk_gate_hft_mm.yml new file mode 100644 index 00000000..8ab1678e --- /dev/null +++ b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/controllers/sndk_gate_hft_mm.yml @@ -0,0 +1,33 @@ +buy_amounts_pct: 4,2,1 +buy_cooldown_time: 8 +buy_position_effectivization_time: 45 +buy_spreads: 0.0002,0.0008,0.0020 +connector_name: gate_io_perpetual +controller_name: pmm_mister +controller_type: generic +executor_refresh_time: 5 +global_stop_loss: 0.025 +global_take_profit: 0.02 +id: sndk_gate_hft_mm +leverage: 5 +manual_kill_switch: false +max_active_executors_by_level: 3 +max_base_pct: 0.75 +min_base_pct: 0.3 +min_skew: 1.8 +open_order_type: 3 +portfolio_allocation: 0.3 +position_mode: HEDGE +position_profit_protection: true +price_distance_tolerance: 0.0002 +refresh_tolerance: 0.0002 +sell_amounts_pct: 2,2,1 +sell_cooldown_time: 12 +sell_position_effectivization_time: 45 +sell_spreads: 0.0004,0.0016,0.0040 +take_profit: 0.0003 +take_profit_order_type: 3 +target_base_pct: 0.55 +tolerance_scaling: 1.3 +total_amount_quote: 46 +trading_pair: SNDK-USDT diff --git a/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/hummingbot_logs.yml b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/hummingbot_logs.yml new file mode 100755 index 00000000..8e65271c --- /dev/null +++ b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/hummingbot_logs.yml @@ -0,0 +1,83 @@ +--- +version: 1 +template_version: 12 + +formatters: + simple: + format: "%(asctime)s - %(process)d - %(name)s - %(levelname)s - %(message)s" + +handlers: + console: + class: hummingbot.logger.cli_handler.CLIHandler + level: DEBUG + formatter: simple + stream: ext://sys.stdout + console_warning: + class: hummingbot.logger.cli_handler.CLIHandler + level: WARNING + formatter: simple + stream: ext://sys.stdout + console_info: + class: hummingbot.logger.cli_handler.CLIHandler + level: INFO + formatter: simple + stream: ext://sys.stdout + file_handler: + class: logging.handlers.TimedRotatingFileHandler + level: DEBUG + formatter: simple + filename: $PROJECT_DIR/logs/logs_$STRATEGY_FILE_PATH.log + encoding: utf8 + when: "D" + interval: 1 + backupCount: 7 + "null": + class: logging.NullHandler + level: DEBUG + +loggers: + hummingbot.core.utils.eth_gas_station_lookup: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.logger.log_server_client: + level: WARNING + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.logger.reporting_proxy_handler: + level: WARNING + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.strategy: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.connector: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.client: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.core.event.event_reporter: + level: EVENT_LOG + propagate: false + handlers: [file_handler] + mqtt: false + conf: + level: NETWORK + handlers: ["null"] + propagate: false + mqtt: false + +root: + level: INFO + handlers: [console, file_handler] + mqtt: true diff --git a/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/scripts/sndk-gate-hft-mm-20260818-030708.yml b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/scripts/sndk-gate-hft-mm-20260818-030708.yml new file mode 100644 index 00000000..f882315d --- /dev/null +++ b/bots/archived/sndk-gate-hft-mm-20260818-030708/conf/scripts/sndk-gate-hft-mm-20260818-030708.yml @@ -0,0 +1,4 @@ +controllers_config: +- sndk_gate_hft_mm.yml +max_controller_drawdown_quote: 10.0 +script_file_name: v2_with_controllers.py diff --git a/bots/archived/sndk-gate-hft-mm-20260818-030708/data/sndk-gate-hft-mm-20260818-030708.sqlite b/bots/archived/sndk-gate-hft-mm-20260818-030708/data/sndk-gate-hft-mm-20260818-030708.sqlite new file mode 100644 index 00000000..6fce8dfc Binary files /dev/null and b/bots/archived/sndk-gate-hft-mm-20260818-030708/data/sndk-gate-hft-mm-20260818-030708.sqlite differ diff --git a/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/.password_verification b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/.password_verification new file mode 100644 index 00000000..d1a274b6 --- /dev/null +++ b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/.password_verification @@ -0,0 +1 @@ +7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20226463613937613132666365383432623664323437613830393966313638373662227d2c202263697068657274657874223a20223537393331373032636666343037396439623430222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226536376664366338373033386336613439653066373938316366656138656539227d2c20226d6163223a202233366134323163646334303464393433343264656166613037623235373866363734383930653961393765303037393231333132326532333464303666323034227d2c202276657273696f6e223a20332c2022616c696173223a2022227d \ No newline at end of file diff --git a/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/conf_client.yml b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/conf_client.yml new file mode 100644 index 00000000..895d6ac2 --- /dev/null +++ b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/conf_client.yml @@ -0,0 +1,179 @@ +#################################### +### client_config_map config ### +#################################### + +instance_id: sndk_gate_hft_mm-20260818-032452 + +# Fetch trading pairs from all exchanges if True, otherwise fetch only from connected exchanges. +fetch_pairs_from_all_exchanges: false + +log_level: INFO + +debug_console: false + +strategy_report_interval: 900.0 + +logger_override_whitelist: +- hummingbot.strategy.arbitrage +- hummingbot.strategy.cross_exchange_market_making +- conf + +log_file_path: /home/hummingbot/logs + +kill_switch_mode: {} + +# What to auto-fill in the prompt after each import command (start/config) +autofill_import: disabled + +# MQTT Bridge configuration. +mqtt_bridge: + mqtt_host: localhost + mqtt_port: 1883 + mqtt_username: '' + mqtt_password: '' + mqtt_namespace: hbot + mqtt_ssl: false + mqtt_logger: true + mqtt_notifier: true + mqtt_commands: true + mqtt_events: true + mqtt_external_events: true + mqtt_autostart: true + +# Error log sharing +send_error_logs: true + +# Advanced database options, currently supports SQLAlchemy's included dialects +# Reference: https://docs.sqlalchemy.org/en/13/dialects/ +# To use an instance of SQLite DB the required configuration is +# db_engine: sqlite +# To use a DBMS the required configuration is +# db_host: 127.0.0.1 +# db_port: 3306 +# db_username: username +# db_password: password +# db_name: dbname +db_mode: + db_engine: sqlite + +# Balance Limit Configurations +# e.g. Setting USDT and BTC limits on Binance. +# balance_asset_limit: +# binance: +# BTC: 0.1 +# USDT: 1000 +balance_asset_limit: + ascend_ex: {} + ascend_ex_paper_trade: {} + binance: {} + binance_paper_trade: {} + binance_us: {} + bitfinex: {} + bitmart: {} + bitmex: {} + bitmex_testnet: {} + btc_markets: {} + bybit: {} + bybit_testnet: {} + coinbase_pro: {} + foxbit: {} + gate_io: {} + gate_io_paper_trade: {} + hitbtc: {} + huobi: {} + injective_v2: {} + kraken: {} + kucoin: {} + kucoin_paper_trade: {} + mexc: {} + mock_paper_exchange: {} + ndax: {} + ndax_testnet: {} + okx: {} + polkadex: {} + vertex: {} + vertex_testnet: {} + woo_x: {} + woo_x_testnet: {} + +# Fixed gas price (in Gwei) for Ethereum transactions +manual_gas_price: 50.0 + +# Gateway API Configurations +# default host to only use localhost +# Port need to match the final installation port for Gateway +gateway: + gateway_api_host: localhost + gateway_api_port: '15888' + gateway_use_ssl: true + +# Whether to enable aggregated order and trade data collection +anonymized_metrics_mode: + anonymized_metrics_interval_min: 15.0 + +# A source for rate oracle, currently binance, coin_gecko, coin_cap, dexalot, evedex_perpetual, decibel_perpetual, kucoin, gate_io, backpack, coinbase_advanced_trade, hyperliquid, hyperliquid_perpetual, architect_perpetual, derive, mexc +rate_oracle_source: + name: gate_io + +# A universal token which to display tokens values in, e.g. USD,EUR,BTC +global_token: + global_token_name: USDT + global_token_symbol: $ + usd_equivalent_tokens: + - USD + +# Percentage of API rate limits (on any exchange and any end point) allocated to this bot instance. +# Enter 50 to indicate 50%. E.g. if the API rate limit is 100 calls per second, and you allocate +# 50% to this setting, the bot will have a maximum (limit) of 50 calls per second +rate_limits_share_pct: 100.0 + +commands_timeout: + create_command_timeout: 10.0 + other_commands_timeout: 30.0 + +# Tabulate table format style (https://github.com/astanin/python-tabulate#table-format) +tables_format: psql + +paper_trade: + paper_trade_exchanges: + - binance + - kucoin + - ascend_ex + - gate_io + paper_trade_account_balance: + BTC: 1.0 + DAI: 1000.0 + ETH: 10.0 + ONE: 1000.0 + TUSD: 1000.0 + USDC: 1000.0 + USDQ: 1000.0 + USDT: 1000.0 + WETH: 10.0 + +color: + top_pane: '#000000' + bottom_pane: '#000000' + output_pane: '#262626' + input_pane: '#1C1C1C' + logs_pane: '#121212' + terminal_primary: '#5FFFD7' + primary_label: '#5FFFD7' + secondary_label: '#FFFFFF' + success_label: '#5FFFD7' + warning_label: '#FFFF00' + info_label: '#5FD7FF' + error_label: '#FF0000' + gold_label: '#FFD700' + silver_label: '#C0C0C0' + bronze_label: '#CD7F32' + +# The tick size is the frequency with which the clock notifies the time iterators by calling the +# c_tick() method, that means for example that if the tick size is 1, the logic of the strategy +# will run every second. +tick_size: 1.0 + +market_data_collection: + market_data_collection_enabled: false + market_data_collection_interval: 60 + market_data_collection_depth: 20 diff --git a/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/conf_fee_overrides.yml b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/conf_fee_overrides.yml new file mode 100644 index 00000000..8dc9d57e --- /dev/null +++ b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/conf_fee_overrides.yml @@ -0,0 +1,526 @@ +######################################## +### Fee overrides configurations ### +######################################## + +# For more detailed information: https://docs.hummingbot.io +template_version: 14 + +# Example of the fields that can be specified to override the `TradeFeeFactory` default settings. +# If the field is missing or the value is left blank, the default value will be used. +# The percentage values are specified as 0.1 for 0.1%. +# +# [exchange name]_percent_fee_token: +# [exchange name]_maker_percent_fee: +# [exchange name]_taker_percent_fee: +# [exchange name]_buy_percent_fee_deducted_from_returns: # if False, the buy fee is added to the order costs +# [exchange name]_maker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]]) +# [exchange name]_taker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]]) + +binance_percent_fee_token: # BNB +binance_maker_percent_fee: # 0.75 +binance_taker_percent_fee: # 0.75 +binance_buy_percent_fee_deducted_from_returns: # True + +# List of supported Exchanges for which the user's conf/conf_fee_override.yml +# will work. This file currently needs to be in sync with hummingbot list of +# supported exchanges +ascend_ex_buy_percent_fee_deducted_from_returns: +ascend_ex_maker_fixed_fees: +ascend_ex_maker_percent_fee: +ascend_ex_percent_fee_token: +ascend_ex_taker_fixed_fees: +ascend_ex_taker_percent_fee: +binance_maker_fixed_fees: +binance_perpetual_buy_percent_fee_deducted_from_returns: +binance_perpetual_maker_fixed_fees: +binance_perpetual_maker_percent_fee: +binance_perpetual_percent_fee_token: +binance_perpetual_taker_fixed_fees: +binance_perpetual_taker_percent_fee: +binance_perpetual_testnet_buy_percent_fee_deducted_from_returns: +binance_perpetual_testnet_maker_fixed_fees: +binance_perpetual_testnet_maker_percent_fee: +binance_perpetual_testnet_percent_fee_token: +binance_perpetual_testnet_taker_fixed_fees: +binance_perpetual_testnet_taker_percent_fee: +binance_taker_fixed_fees: +binance_us_buy_percent_fee_deducted_from_returns: +binance_us_maker_fixed_fees: +binance_us_maker_percent_fee: +binance_us_percent_fee_token: +binance_us_taker_fixed_fees: +binance_us_taker_percent_fee: +bitfinex_buy_percent_fee_deducted_from_returns: +bitfinex_maker_fixed_fees: +bitfinex_maker_percent_fee: +bitfinex_percent_fee_token: +bitfinex_taker_fixed_fees: +bitfinex_taker_percent_fee: +bitmart_buy_percent_fee_deducted_from_returns: +bitmart_maker_fixed_fees: +bitmart_maker_percent_fee: +bitmart_percent_fee_token: +bitmart_taker_fixed_fees: +bitmart_taker_percent_fee: +btc_markets_percent_fee_token: +btc_markets_maker_percent_fee: +btc_markets_taker_percent_fee: +btc_markets_buy_percent_fee_deducted_from_returns: +bybit_perpetual_buy_percent_fee_deducted_from_returns: +bybit_perpetual_maker_fixed_fees: +bybit_perpetual_maker_percent_fee: +bybit_perpetual_percent_fee_token: +bybit_perpetual_taker_fixed_fees: +bybit_perpetual_taker_percent_fee: +bybit_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bybit_perpetual_testnet_maker_fixed_fees: +bybit_perpetual_testnet_maker_percent_fee: +bybit_perpetual_testnet_percent_fee_token: +bybit_perpetual_testnet_taker_fixed_fees: +bybit_perpetual_testnet_taker_percent_fee: +coinbase_pro_buy_percent_fee_deducted_from_returns: +coinbase_pro_maker_fixed_fees: +coinbase_pro_maker_percent_fee: +coinbase_pro_percent_fee_token: +coinbase_pro_taker_fixed_fees: +coinbase_pro_taker_percent_fee: +dydx_perpetual_buy_percent_fee_deducted_from_returns: +dydx_perpetual_maker_fixed_fees: +dydx_perpetual_maker_percent_fee: +dydx_perpetual_percent_fee_token: +dydx_perpetual_taker_fixed_fees: +dydx_perpetual_taker_percent_fee: +gate_io_buy_percent_fee_deducted_from_returns: +gate_io_maker_fixed_fees: +gate_io_maker_percent_fee: +gate_io_percent_fee_token: +gate_io_taker_fixed_fees: +gate_io_taker_percent_fee: +hitbtc_buy_percent_fee_deducted_from_returns: +hitbtc_maker_fixed_fees: +hitbtc_maker_percent_fee: +hitbtc_percent_fee_token: +hitbtc_taker_fixed_fees: +hitbtc_taker_percent_fee: +huobi_buy_percent_fee_deducted_from_returns: +huobi_maker_fixed_fees: +huobi_maker_percent_fee: +huobi_percent_fee_token: +huobi_taker_fixed_fees: +huobi_taker_percent_fee: +kraken_buy_percent_fee_deducted_from_returns: +kraken_maker_fixed_fees: +kraken_maker_percent_fee: +kraken_percent_fee_token: +kraken_taker_fixed_fees: +kraken_taker_percent_fee: +kucoin_buy_percent_fee_deducted_from_returns: +kucoin_maker_fixed_fees: +kucoin_maker_percent_fee: +kucoin_percent_fee_token: +kucoin_taker_fixed_fees: +kucoin_taker_percent_fee: +mexc_buy_percent_fee_deducted_from_returns: +mexc_maker_fixed_fees: +mexc_maker_percent_fee: +mexc_percent_fee_token: +mexc_taker_fixed_fees: +mexc_taker_percent_fee: +ndax_buy_percent_fee_deducted_from_returns: +ndax_maker_fixed_fees: +ndax_maker_percent_fee: +ndax_percent_fee_token: +ndax_taker_fixed_fees: +ndax_taker_percent_fee: +ndax_testnet_buy_percent_fee_deducted_from_returns: +ndax_testnet_maker_fixed_fees: +ndax_testnet_maker_percent_fee: +ndax_testnet_percent_fee_token: +ndax_testnet_taker_fixed_fees: +ndax_testnet_taker_percent_fee: +okx_buy_percent_fee_deducted_from_returns: +okx_maker_fixed_fees: +okx_maker_percent_fee: +okx_percent_fee_token: +okx_taker_fixed_fees: +okx_taker_percent_fee: +phemex_perpetual_percent_fee_token: +phemex_perpetual_maker_percent_fee: +phemex_perpetual_taker_percent_fee: +phemex_perpetual_buy_percent_fee_deducted_from_returns: +phemex_perpetual_maker_fixed_fees: +phemex_perpetual_taker_fixed_fees: +phemex_perpetual_testnet_percent_fee_token: +phemex_perpetual_testnet_maker_percent_fee: +phemex_perpetual_testnet_taker_percent_fee: +phemex_perpetual_testnet_buy_percent_fee_deducted_from_returns: +phemex_perpetual_testnet_maker_fixed_fees: +phemex_perpetual_testnet_taker_fixed_fees: +injective_v2_perpetual_percent_fee_token: +injective_v2_perpetual_maker_percent_fee: +injective_v2_perpetual_taker_percent_fee: +injective_v2_perpetual_buy_percent_fee_deducted_from_returns: +injective_v2_perpetual_maker_fixed_fees: +injective_v2_perpetual_taker_fixed_fees: +bitget_perpetual_percent_fee_token: +bitget_perpetual_maker_percent_fee: +bitget_perpetual_taker_percent_fee: +bitget_perpetual_buy_percent_fee_deducted_from_returns: +bitget_perpetual_maker_fixed_fees: +bitget_perpetual_taker_fixed_fees: +bit_com_perpetual_percent_fee_token: +bit_com_perpetual_maker_percent_fee: +bit_com_perpetual_taker_percent_fee: +bit_com_perpetual_buy_percent_fee_deducted_from_returns: +bit_com_perpetual_maker_fixed_fees: +bit_com_perpetual_taker_fixed_fees: +bit_com_perpetual_testnet_percent_fee_token: +bit_com_perpetual_testnet_maker_percent_fee: +bit_com_perpetual_testnet_taker_percent_fee: +bit_com_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bit_com_perpetual_testnet_maker_fixed_fees: +bit_com_perpetual_testnet_taker_fixed_fees: +gate_io_perpetual_percent_fee_token: +gate_io_perpetual_maker_percent_fee: +gate_io_perpetual_taker_percent_fee: +gate_io_perpetual_buy_percent_fee_deducted_from_returns: +gate_io_perpetual_maker_fixed_fees: +gate_io_perpetual_taker_fixed_fees: +bitmex_perpetual_percent_fee_token: +bitmex_perpetual_maker_percent_fee: +bitmex_perpetual_taker_percent_fee: +bitmex_perpetual_buy_percent_fee_deducted_from_returns: +bitmex_perpetual_maker_fixed_fees: +bitmex_perpetual_taker_fixed_fees: +bitmex_perpetual_testnet_percent_fee_token: +bitmex_perpetual_testnet_maker_percent_fee: +bitmex_perpetual_testnet_taker_percent_fee: +bitmex_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bitmex_perpetual_testnet_maker_fixed_fees: +bitmex_perpetual_testnet_taker_fixed_fees: +kucoin_perpetual_percent_fee_token: +kucoin_perpetual_maker_percent_fee: +kucoin_perpetual_taker_percent_fee: +kucoin_perpetual_buy_percent_fee_deducted_from_returns: +kucoin_perpetual_maker_fixed_fees: +kucoin_perpetual_taker_fixed_fees: +bitmex_percent_fee_token: +bitmex_maker_percent_fee: +bitmex_taker_percent_fee: +bitmex_buy_percent_fee_deducted_from_returns: +bitmex_maker_fixed_fees: +bitmex_taker_fixed_fees: +bitmex_testnet_percent_fee_token: +bitmex_testnet_maker_percent_fee: +bitmex_testnet_taker_percent_fee: +bitmex_testnet_buy_percent_fee_deducted_from_returns: +bitmex_testnet_maker_fixed_fees: +bitmex_testnet_taker_fixed_fees: +btc_markets_maker_fixed_fees: +btc_markets_taker_fixed_fees: +woo_x_percent_fee_token: +woo_x_maker_percent_fee: +woo_x_taker_percent_fee: +woo_x_buy_percent_fee_deducted_from_returns: +woo_x_maker_fixed_fees: +woo_x_taker_fixed_fees: +woo_x_testnet_percent_fee_token: +woo_x_testnet_maker_percent_fee: +woo_x_testnet_taker_percent_fee: +woo_x_testnet_buy_percent_fee_deducted_from_returns: +woo_x_testnet_maker_fixed_fees: +woo_x_testnet_taker_fixed_fees: +polkadex_percent_fee_token: +polkadex_maker_percent_fee: +polkadex_taker_percent_fee: +polkadex_buy_percent_fee_deducted_from_returns: +polkadex_maker_fixed_fees: +polkadex_taker_fixed_fees: +bybit_percent_fee_token: +bybit_maker_percent_fee: +bybit_taker_percent_fee: +bybit_buy_percent_fee_deducted_from_returns: +bybit_maker_fixed_fees: +bybit_taker_fixed_fees: +bybit_testnet_percent_fee_token: +bybit_testnet_maker_percent_fee: +bybit_testnet_taker_percent_fee: +bybit_testnet_buy_percent_fee_deducted_from_returns: +bybit_testnet_maker_fixed_fees: +bybit_testnet_taker_fixed_fees: +injective_v2_percent_fee_token: +injective_v2_maker_percent_fee: +injective_v2_taker_percent_fee: +injective_v2_buy_percent_fee_deducted_from_returns: +injective_v2_maker_fixed_fees: +injective_v2_taker_fixed_fees: +vertex_percent_fee_token: +vertex_maker_percent_fee: +vertex_taker_percent_fee: +vertex_buy_percent_fee_deducted_from_returns: +vertex_maker_fixed_fees: +vertex_taker_fixed_fees: +vertex_testnet_percent_fee_token: +vertex_testnet_maker_percent_fee: +vertex_testnet_taker_percent_fee: +vertex_testnet_buy_percent_fee_deducted_from_returns: +vertex_testnet_maker_fixed_fees: +vertex_testnet_taker_fixed_fees: +mock_paper_exchange_percent_fee_token: +mock_paper_exchange_maker_percent_fee: +mock_paper_exchange_taker_percent_fee: +mock_paper_exchange_buy_percent_fee_deducted_from_returns: +mock_paper_exchange_maker_fixed_fees: +mock_paper_exchange_taker_fixed_fees: +kucoin_perpetual_testnet_percent_fee_token: +kucoin_perpetual_testnet_maker_percent_fee: +kucoin_perpetual_testnet_taker_percent_fee: +kucoin_perpetual_testnet_buy_percent_fee_deducted_from_returns: +kucoin_perpetual_testnet_maker_fixed_fees: +kucoin_perpetual_testnet_taker_fixed_fees: +foxbit_percent_fee_token: +foxbit_maker_percent_fee: +foxbit_taker_percent_fee: +foxbit_buy_percent_fee_deducted_from_returns: +foxbit_maker_fixed_fees: +foxbit_taker_fixed_fees: +hyperliquid_perpetual_percent_fee_token: +hyperliquid_perpetual_maker_percent_fee: +hyperliquid_perpetual_taker_percent_fee: +hyperliquid_perpetual_buy_percent_fee_deducted_from_returns: +hyperliquid_perpetual_maker_fixed_fees: +hyperliquid_perpetual_taker_fixed_fees: +hyperliquid_perpetual_testnet_percent_fee_token: +hyperliquid_perpetual_testnet_maker_percent_fee: +hyperliquid_perpetual_testnet_taker_percent_fee: +hyperliquid_perpetual_testnet_buy_percent_fee_deducted_from_returns: +hyperliquid_perpetual_testnet_maker_fixed_fees: +hyperliquid_perpetual_testnet_taker_fixed_fees: +bitmart_perpetual_percent_fee_token: +bitmart_perpetual_maker_percent_fee: +bitmart_perpetual_taker_percent_fee: +bitmart_perpetual_buy_percent_fee_deducted_from_returns: +bitmart_perpetual_maker_fixed_fees: +bitmart_perpetual_taker_fixed_fees: +architect_perpetual_percent_fee_token: +architect_perpetual_maker_percent_fee: +architect_perpetual_taker_percent_fee: +architect_perpetual_buy_percent_fee_deducted_from_returns: +architect_perpetual_maker_fixed_fees: +architect_perpetual_taker_fixed_fees: +architect_perpetual_sandbox_percent_fee_token: +architect_perpetual_sandbox_maker_percent_fee: +architect_perpetual_sandbox_taker_percent_fee: +architect_perpetual_sandbox_buy_percent_fee_deducted_from_returns: +architect_perpetual_sandbox_maker_fixed_fees: +architect_perpetual_sandbox_taker_fixed_fees: +pacifica_perpetual_percent_fee_token: +pacifica_perpetual_maker_percent_fee: +pacifica_perpetual_taker_percent_fee: +pacifica_perpetual_buy_percent_fee_deducted_from_returns: +pacifica_perpetual_maker_fixed_fees: +pacifica_perpetual_taker_fixed_fees: +pacifica_perpetual_testnet_percent_fee_token: +pacifica_perpetual_testnet_maker_percent_fee: +pacifica_perpetual_testnet_taker_percent_fee: +pacifica_perpetual_testnet_buy_percent_fee_deducted_from_returns: +pacifica_perpetual_testnet_maker_fixed_fees: +pacifica_perpetual_testnet_taker_fixed_fees: +lighter_perpetual_percent_fee_token: +lighter_perpetual_maker_percent_fee: +lighter_perpetual_taker_percent_fee: +lighter_perpetual_buy_percent_fee_deducted_from_returns: +lighter_perpetual_maker_fixed_fees: +lighter_perpetual_taker_fixed_fees: +lighter_perpetual_testnet_percent_fee_token: +lighter_perpetual_testnet_maker_percent_fee: +lighter_perpetual_testnet_taker_percent_fee: +lighter_perpetual_testnet_buy_percent_fee_deducted_from_returns: +lighter_perpetual_testnet_maker_fixed_fees: +lighter_perpetual_testnet_taker_fixed_fees: +derive_perpetual_percent_fee_token: +derive_perpetual_maker_percent_fee: +derive_perpetual_taker_percent_fee: +derive_perpetual_buy_percent_fee_deducted_from_returns: +derive_perpetual_maker_fixed_fees: +derive_perpetual_taker_fixed_fees: +derive_perpetual_testnet_percent_fee_token: +derive_perpetual_testnet_maker_percent_fee: +derive_perpetual_testnet_taker_percent_fee: +derive_perpetual_testnet_buy_percent_fee_deducted_from_returns: +derive_perpetual_testnet_maker_fixed_fees: +derive_perpetual_testnet_taker_fixed_fees: +aevo_perpetual_percent_fee_token: +aevo_perpetual_maker_percent_fee: +aevo_perpetual_taker_percent_fee: +aevo_perpetual_buy_percent_fee_deducted_from_returns: +aevo_perpetual_maker_fixed_fees: +aevo_perpetual_taker_fixed_fees: +aevo_perpetual_testnet_percent_fee_token: +aevo_perpetual_testnet_maker_percent_fee: +aevo_perpetual_testnet_taker_percent_fee: +aevo_perpetual_testnet_buy_percent_fee_deducted_from_returns: +aevo_perpetual_testnet_maker_fixed_fees: +aevo_perpetual_testnet_taker_fixed_fees: +dydx_v4_perpetual_percent_fee_token: +dydx_v4_perpetual_maker_percent_fee: +dydx_v4_perpetual_taker_percent_fee: +dydx_v4_perpetual_buy_percent_fee_deducted_from_returns: +dydx_v4_perpetual_maker_fixed_fees: +dydx_v4_perpetual_taker_fixed_fees: +okx_perpetual_percent_fee_token: +okx_perpetual_maker_percent_fee: +okx_perpetual_taker_percent_fee: +okx_perpetual_buy_percent_fee_deducted_from_returns: +okx_perpetual_maker_fixed_fees: +okx_perpetual_taker_fixed_fees: +grvt_perpetual_percent_fee_token: +grvt_perpetual_maker_percent_fee: +grvt_perpetual_taker_percent_fee: +grvt_perpetual_buy_percent_fee_deducted_from_returns: +grvt_perpetual_maker_fixed_fees: +grvt_perpetual_taker_fixed_fees: +grvt_perpetual_testnet_percent_fee_token: +grvt_perpetual_testnet_maker_percent_fee: +grvt_perpetual_testnet_taker_percent_fee: +grvt_perpetual_testnet_buy_percent_fee_deducted_from_returns: +grvt_perpetual_testnet_maker_fixed_fees: +grvt_perpetual_testnet_taker_fixed_fees: +evedex_perpetual_percent_fee_token: +evedex_perpetual_maker_percent_fee: +evedex_perpetual_taker_percent_fee: +evedex_perpetual_buy_percent_fee_deducted_from_returns: +evedex_perpetual_maker_fixed_fees: +evedex_perpetual_taker_fixed_fees: +backpack_perpetual_percent_fee_token: +backpack_perpetual_maker_percent_fee: +backpack_perpetual_taker_percent_fee: +backpack_perpetual_buy_percent_fee_deducted_from_returns: +backpack_perpetual_maker_fixed_fees: +backpack_perpetual_taker_fixed_fees: +decibel_perpetual_percent_fee_token: +decibel_perpetual_maker_percent_fee: +decibel_perpetual_taker_percent_fee: +decibel_perpetual_buy_percent_fee_deducted_from_returns: +decibel_perpetual_maker_fixed_fees: +decibel_perpetual_taker_fixed_fees: +decibel_perpetual_testnet_percent_fee_token: +decibel_perpetual_testnet_maker_percent_fee: +decibel_perpetual_testnet_taker_percent_fee: +decibel_perpetual_testnet_buy_percent_fee_deducted_from_returns: +decibel_perpetual_testnet_maker_fixed_fees: +decibel_perpetual_testnet_taker_fixed_fees: +derive_percent_fee_token: +derive_maker_percent_fee: +derive_taker_percent_fee: +derive_buy_percent_fee_deducted_from_returns: +derive_maker_fixed_fees: +derive_taker_fixed_fees: +derive_testnet_percent_fee_token: +derive_testnet_maker_percent_fee: +derive_testnet_taker_percent_fee: +derive_testnet_buy_percent_fee_deducted_from_returns: +derive_testnet_maker_fixed_fees: +derive_testnet_taker_fixed_fees: +bitrue_percent_fee_token: +bitrue_maker_percent_fee: +bitrue_taker_percent_fee: +bitrue_buy_percent_fee_deducted_from_returns: +bitrue_maker_fixed_fees: +bitrue_taker_fixed_fees: +lambdaplex_percent_fee_token: +lambdaplex_maker_percent_fee: +lambdaplex_taker_percent_fee: +lambdaplex_buy_percent_fee_deducted_from_returns: +lambdaplex_maker_fixed_fees: +lambdaplex_taker_fixed_fees: +dexalot_percent_fee_token: +dexalot_maker_percent_fee: +dexalot_taker_percent_fee: +dexalot_buy_percent_fee_deducted_from_returns: +dexalot_maker_fixed_fees: +dexalot_taker_fixed_fees: +dexalot_testnet_percent_fee_token: +dexalot_testnet_maker_percent_fee: +dexalot_testnet_taker_percent_fee: +dexalot_testnet_buy_percent_fee_deducted_from_returns: +dexalot_testnet_maker_fixed_fees: +dexalot_testnet_taker_fixed_fees: +bing_x_percent_fee_token: +bing_x_maker_percent_fee: +bing_x_taker_percent_fee: +bing_x_buy_percent_fee_deducted_from_returns: +bing_x_maker_fixed_fees: +bing_x_taker_fixed_fees: +kucoin_hft_percent_fee_token: +kucoin_hft_maker_percent_fee: +kucoin_hft_taker_percent_fee: +kucoin_hft_buy_percent_fee_deducted_from_returns: +kucoin_hft_maker_fixed_fees: +kucoin_hft_taker_fixed_fees: +bitstamp_percent_fee_token: +bitstamp_maker_percent_fee: +bitstamp_taker_percent_fee: +bitstamp_buy_percent_fee_deducted_from_returns: +bitstamp_maker_fixed_fees: +bitstamp_taker_fixed_fees: +gemini_percent_fee_token: +gemini_maker_percent_fee: +gemini_taker_percent_fee: +gemini_buy_percent_fee_deducted_from_returns: +gemini_maker_fixed_fees: +gemini_taker_fixed_fees: +backpack_percent_fee_token: +backpack_maker_percent_fee: +backpack_taker_percent_fee: +backpack_buy_percent_fee_deducted_from_returns: +backpack_maker_fixed_fees: +backpack_taker_fixed_fees: +htx_percent_fee_token: +htx_maker_percent_fee: +htx_taker_percent_fee: +htx_buy_percent_fee_deducted_from_returns: +htx_maker_fixed_fees: +htx_taker_fixed_fees: +lighter_percent_fee_token: +lighter_maker_percent_fee: +lighter_taker_percent_fee: +lighter_buy_percent_fee_deducted_from_returns: +lighter_maker_fixed_fees: +lighter_taker_fixed_fees: +lighter_testnet_percent_fee_token: +lighter_testnet_maker_percent_fee: +lighter_testnet_taker_percent_fee: +lighter_testnet_buy_percent_fee_deducted_from_returns: +lighter_testnet_maker_fixed_fees: +lighter_testnet_taker_fixed_fees: +hyperliquid_percent_fee_token: +hyperliquid_maker_percent_fee: +hyperliquid_taker_percent_fee: +hyperliquid_buy_percent_fee_deducted_from_returns: +hyperliquid_maker_fixed_fees: +hyperliquid_taker_fixed_fees: +hyperliquid_testnet_percent_fee_token: +hyperliquid_testnet_maker_percent_fee: +hyperliquid_testnet_taker_percent_fee: +hyperliquid_testnet_buy_percent_fee_deducted_from_returns: +hyperliquid_testnet_maker_fixed_fees: +hyperliquid_testnet_taker_fixed_fees: +coinbase_advanced_trade_percent_fee_token: +coinbase_advanced_trade_maker_percent_fee: +coinbase_advanced_trade_taker_percent_fee: +coinbase_advanced_trade_buy_percent_fee_deducted_from_returns: +coinbase_advanced_trade_maker_fixed_fees: +coinbase_advanced_trade_taker_fixed_fees: +xrpl_percent_fee_token: +xrpl_maker_percent_fee: +xrpl_taker_percent_fee: +xrpl_buy_percent_fee_deducted_from_returns: +xrpl_maker_fixed_fees: +xrpl_taker_fixed_fees: +bitget_percent_fee_token: +bitget_maker_percent_fee: +bitget_taker_percent_fee: +bitget_buy_percent_fee_deducted_from_returns: +bitget_maker_fixed_fees: +bitget_taker_fixed_fees: diff --git a/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/connectors/.dockerignore b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/connectors/.dockerignore new file mode 100644 index 00000000..ad30bfec --- /dev/null +++ b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/connectors/.dockerignore @@ -0,0 +1 @@ +*.yml \ No newline at end of file diff --git a/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/connectors/.gitignore b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/connectors/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/connectors/gate_io_perpetual.yml b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/connectors/gate_io_perpetual.yml new file mode 100644 index 00000000..503e58d7 --- /dev/null +++ b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/connectors/gate_io_perpetual.yml @@ -0,0 +1,11 @@ +#################################### +### gate_io_perpetual config ### +#################################### + +connector: gate_io_perpetual + +gate_io_perpetual_api_key: 7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20223536666463386230626433613261353136633462393737313437353638653139227d2c202263697068657274657874223a202235376637663363336662626464646562386632666465316637643039306231393234656265393133643664353836653362636537316537643561363138613561222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20223731316464306439376336316430306331353461306362633663613833313364227d2c20226d6163223a202236303137366133306439313765376663623436623432316639376430356637326335343266363238633034383164646136653735643638653732623663613238227d2c202276657273696f6e223a20332c2022616c696173223a2022227d + +gate_io_perpetual_secret_key: 7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20223734303930346566636339636331613064313163393065346133646263616264227d2c202263697068657274657874223a20223665653731376164313439326139363463366539643036633662386132666661373532626636653831393965653963346333356239336663616562633738346335656235626231626166666365646662336361646633336164386630623935306466306163333763303934376165356236613761646238326466613134626333222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226332396631646230333065323564363734643663306365643166383161313462227d2c20226d6163223a202265376239346564316564303537646434366639356537646166653464326533386465326330353365636261356534613662636335623232313566393164393963227d2c202276657273696f6e223a20332c2022616c696173223a2022227d + +gate_io_perpetual_user_id: 7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20226165613865323531633839323037663132373166323739653233633835356261227d2c202263697068657274657874223a20226338663061646538666230353335222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226438303663636334663833366538636662613366323761396361643364623765227d2c20226d6163223a202230383430646330336463646636313263383433326262373330623466613133363333613136626562373637306533363031656265343838653461646636333634227d2c202276657273696f6e223a20332c2022616c696173223a2022227d diff --git a/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/controllers/sndk_gate_hft_mm.yml b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/controllers/sndk_gate_hft_mm.yml new file mode 100644 index 00000000..fda6b929 --- /dev/null +++ b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/controllers/sndk_gate_hft_mm.yml @@ -0,0 +1,33 @@ +buy_amounts_pct: 4,2,1 +buy_cooldown_time: 8 +buy_position_effectivization_time: 45 +buy_spreads: 0.0002,0.0008,0.0020 +connector_name: gate_io_perpetual +controller_name: pmm_mister +controller_type: generic +executor_refresh_time: 5 +global_stop_loss: 0.025 +global_take_profit: 0.02 +id: sndk_gate_hft_mm +leverage: 5 +manual_kill_switch: false +max_active_executors_by_level: 3 +max_base_pct: 0.75 +min_base_pct: 0.3 +min_skew: 1.8 +open_order_type: 3 +portfolio_allocation: 0.3 +position_mode: HEDGE +position_profit_protection: true +price_distance_tolerance: 0.0002 +refresh_tolerance: 0.0002 +sell_amounts_pct: 2,2,1 +sell_cooldown_time: 12 +sell_position_effectivization_time: 45 +sell_spreads: 0.0004,0.0016,0.0040 +take_profit: 0.0003 +take_profit_order_type: 3 +target_base_pct: 0.55 +tolerance_scaling: 1.3 +total_amount_quote: 100 +trading_pair: SNDK-USDT diff --git a/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/hummingbot_logs.yml b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/hummingbot_logs.yml new file mode 100755 index 00000000..8e65271c --- /dev/null +++ b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/hummingbot_logs.yml @@ -0,0 +1,83 @@ +--- +version: 1 +template_version: 12 + +formatters: + simple: + format: "%(asctime)s - %(process)d - %(name)s - %(levelname)s - %(message)s" + +handlers: + console: + class: hummingbot.logger.cli_handler.CLIHandler + level: DEBUG + formatter: simple + stream: ext://sys.stdout + console_warning: + class: hummingbot.logger.cli_handler.CLIHandler + level: WARNING + formatter: simple + stream: ext://sys.stdout + console_info: + class: hummingbot.logger.cli_handler.CLIHandler + level: INFO + formatter: simple + stream: ext://sys.stdout + file_handler: + class: logging.handlers.TimedRotatingFileHandler + level: DEBUG + formatter: simple + filename: $PROJECT_DIR/logs/logs_$STRATEGY_FILE_PATH.log + encoding: utf8 + when: "D" + interval: 1 + backupCount: 7 + "null": + class: logging.NullHandler + level: DEBUG + +loggers: + hummingbot.core.utils.eth_gas_station_lookup: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.logger.log_server_client: + level: WARNING + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.logger.reporting_proxy_handler: + level: WARNING + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.strategy: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.connector: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.client: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.core.event.event_reporter: + level: EVENT_LOG + propagate: false + handlers: [file_handler] + mqtt: false + conf: + level: NETWORK + handlers: ["null"] + propagate: false + mqtt: false + +root: + level: INFO + handlers: [console, file_handler] + mqtt: true diff --git a/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/scripts/sndk_gate_hft_mm-20260818-032452.yml b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/scripts/sndk_gate_hft_mm-20260818-032452.yml new file mode 100644 index 00000000..1d14777b --- /dev/null +++ b/bots/archived/sndk_gate_hft_mm-20260818-032452/conf/scripts/sndk_gate_hft_mm-20260818-032452.yml @@ -0,0 +1,5 @@ +controllers_config: +- sndk_gate_hft_mm.yml +max_controller_drawdown_quote: 10.0 +max_global_drawdown_quote: 15.0 +script_file_name: v2_with_controllers.py diff --git a/bots/archived/sndk_gate_hft_mm-20260818-032452/data/sndk_gate_hft_mm-20260818-032452.sqlite b/bots/archived/sndk_gate_hft_mm-20260818-032452/data/sndk_gate_hft_mm-20260818-032452.sqlite new file mode 100644 index 00000000..d5308aa2 Binary files /dev/null and b/bots/archived/sndk_gate_hft_mm-20260818-032452/data/sndk_gate_hft_mm-20260818-032452.sqlite differ diff --git a/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/.password_verification b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/.password_verification new file mode 100644 index 00000000..d1a274b6 --- /dev/null +++ b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/.password_verification @@ -0,0 +1 @@ +7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20226463613937613132666365383432623664323437613830393966313638373662227d2c202263697068657274657874223a20223537393331373032636666343037396439623430222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226536376664366338373033386336613439653066373938316366656138656539227d2c20226d6163223a202233366134323163646334303464393433343264656166613037623235373866363734383930653961393765303037393231333132326532333464303666323034227d2c202276657273696f6e223a20332c2022616c696173223a2022227d \ No newline at end of file diff --git a/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/conf_client.yml b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/conf_client.yml new file mode 100644 index 00000000..b41480dd --- /dev/null +++ b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/conf_client.yml @@ -0,0 +1,179 @@ +#################################### +### client_config_map config ### +#################################### + +instance_id: xrpl-pmm-usdc-xrp-20260807-171512 + +# Fetch trading pairs from all exchanges if True, otherwise fetch only from connected exchanges. +fetch_pairs_from_all_exchanges: false + +log_level: INFO + +debug_console: false + +strategy_report_interval: 900.0 + +logger_override_whitelist: +- hummingbot.strategy.arbitrage +- hummingbot.strategy.cross_exchange_market_making +- conf + +log_file_path: /home/hummingbot/logs + +kill_switch_mode: {} + +# What to auto-fill in the prompt after each import command (start/config) +autofill_import: disabled + +# MQTT Bridge configuration. +mqtt_bridge: + mqtt_host: localhost + mqtt_port: 1883 + mqtt_username: '' + mqtt_password: '' + mqtt_namespace: hbot + mqtt_ssl: false + mqtt_logger: true + mqtt_notifier: true + mqtt_commands: true + mqtt_events: true + mqtt_external_events: true + mqtt_autostart: true + +# Error log sharing +send_error_logs: true + +# Advanced database options, currently supports SQLAlchemy's included dialects +# Reference: https://docs.sqlalchemy.org/en/13/dialects/ +# To use an instance of SQLite DB the required configuration is +# db_engine: sqlite +# To use a DBMS the required configuration is +# db_host: 127.0.0.1 +# db_port: 3306 +# db_username: username +# db_password: password +# db_name: dbname +db_mode: + db_engine: sqlite + +# Balance Limit Configurations +# e.g. Setting USDT and BTC limits on Binance. +# balance_asset_limit: +# binance: +# BTC: 0.1 +# USDT: 1000 +balance_asset_limit: + ascend_ex: {} + ascend_ex_paper_trade: {} + binance: {} + binance_paper_trade: {} + binance_us: {} + bitfinex: {} + bitmart: {} + bitmex: {} + bitmex_testnet: {} + btc_markets: {} + bybit: {} + bybit_testnet: {} + coinbase_pro: {} + foxbit: {} + gate_io: {} + gate_io_paper_trade: {} + hitbtc: {} + huobi: {} + injective_v2: {} + kraken: {} + kucoin: {} + kucoin_paper_trade: {} + mexc: {} + mock_paper_exchange: {} + ndax: {} + ndax_testnet: {} + okx: {} + polkadex: {} + vertex: {} + vertex_testnet: {} + woo_x: {} + woo_x_testnet: {} + +# Fixed gas price (in Gwei) for Ethereum transactions +manual_gas_price: 50.0 + +# Gateway API Configurations +# default host to only use localhost +# Port need to match the final installation port for Gateway +gateway: + gateway_api_host: localhost + gateway_api_port: '15888' + gateway_use_ssl: true + +# Whether to enable aggregated order and trade data collection +anonymized_metrics_mode: + anonymized_metrics_interval_min: 15.0 + +# A source for rate oracle, currently binance, coin_gecko, coin_cap, dexalot, evedex_perpetual, decibel_perpetual, kucoin, gate_io, backpack, coinbase_advanced_trade, hyperliquid, hyperliquid_perpetual, architect_perpetual, derive, mexc +rate_oracle_source: + name: gate_io + +# A universal token which to display tokens values in, e.g. USD,EUR,BTC +global_token: + global_token_name: USDT + global_token_symbol: $ + usd_equivalent_tokens: + - USD + +# Percentage of API rate limits (on any exchange and any end point) allocated to this bot instance. +# Enter 50 to indicate 50%. E.g. if the API rate limit is 100 calls per second, and you allocate +# 50% to this setting, the bot will have a maximum (limit) of 50 calls per second +rate_limits_share_pct: 100.0 + +commands_timeout: + create_command_timeout: 10.0 + other_commands_timeout: 30.0 + +# Tabulate table format style (https://github.com/astanin/python-tabulate#table-format) +tables_format: psql + +paper_trade: + paper_trade_exchanges: + - binance + - kucoin + - ascend_ex + - gate_io + paper_trade_account_balance: + BTC: 1.0 + DAI: 1000.0 + ETH: 10.0 + ONE: 1000.0 + TUSD: 1000.0 + USDC: 1000.0 + USDQ: 1000.0 + USDT: 1000.0 + WETH: 10.0 + +color: + top_pane: '#000000' + bottom_pane: '#000000' + output_pane: '#262626' + input_pane: '#1C1C1C' + logs_pane: '#121212' + terminal_primary: '#5FFFD7' + primary_label: '#5FFFD7' + secondary_label: '#FFFFFF' + success_label: '#5FFFD7' + warning_label: '#FFFF00' + info_label: '#5FD7FF' + error_label: '#FF0000' + gold_label: '#FFD700' + silver_label: '#C0C0C0' + bronze_label: '#CD7F32' + +# The tick size is the frequency with which the clock notifies the time iterators by calling the +# c_tick() method, that means for example that if the tick size is 1, the logic of the strategy +# will run every second. +tick_size: 1.0 + +market_data_collection: + market_data_collection_enabled: false + market_data_collection_interval: 60 + market_data_collection_depth: 20 diff --git a/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/conf_fee_overrides.yml b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/conf_fee_overrides.yml new file mode 100644 index 00000000..8dc9d57e --- /dev/null +++ b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/conf_fee_overrides.yml @@ -0,0 +1,526 @@ +######################################## +### Fee overrides configurations ### +######################################## + +# For more detailed information: https://docs.hummingbot.io +template_version: 14 + +# Example of the fields that can be specified to override the `TradeFeeFactory` default settings. +# If the field is missing or the value is left blank, the default value will be used. +# The percentage values are specified as 0.1 for 0.1%. +# +# [exchange name]_percent_fee_token: +# [exchange name]_maker_percent_fee: +# [exchange name]_taker_percent_fee: +# [exchange name]_buy_percent_fee_deducted_from_returns: # if False, the buy fee is added to the order costs +# [exchange name]_maker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]]) +# [exchange name]_taker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]]) + +binance_percent_fee_token: # BNB +binance_maker_percent_fee: # 0.75 +binance_taker_percent_fee: # 0.75 +binance_buy_percent_fee_deducted_from_returns: # True + +# List of supported Exchanges for which the user's conf/conf_fee_override.yml +# will work. This file currently needs to be in sync with hummingbot list of +# supported exchanges +ascend_ex_buy_percent_fee_deducted_from_returns: +ascend_ex_maker_fixed_fees: +ascend_ex_maker_percent_fee: +ascend_ex_percent_fee_token: +ascend_ex_taker_fixed_fees: +ascend_ex_taker_percent_fee: +binance_maker_fixed_fees: +binance_perpetual_buy_percent_fee_deducted_from_returns: +binance_perpetual_maker_fixed_fees: +binance_perpetual_maker_percent_fee: +binance_perpetual_percent_fee_token: +binance_perpetual_taker_fixed_fees: +binance_perpetual_taker_percent_fee: +binance_perpetual_testnet_buy_percent_fee_deducted_from_returns: +binance_perpetual_testnet_maker_fixed_fees: +binance_perpetual_testnet_maker_percent_fee: +binance_perpetual_testnet_percent_fee_token: +binance_perpetual_testnet_taker_fixed_fees: +binance_perpetual_testnet_taker_percent_fee: +binance_taker_fixed_fees: +binance_us_buy_percent_fee_deducted_from_returns: +binance_us_maker_fixed_fees: +binance_us_maker_percent_fee: +binance_us_percent_fee_token: +binance_us_taker_fixed_fees: +binance_us_taker_percent_fee: +bitfinex_buy_percent_fee_deducted_from_returns: +bitfinex_maker_fixed_fees: +bitfinex_maker_percent_fee: +bitfinex_percent_fee_token: +bitfinex_taker_fixed_fees: +bitfinex_taker_percent_fee: +bitmart_buy_percent_fee_deducted_from_returns: +bitmart_maker_fixed_fees: +bitmart_maker_percent_fee: +bitmart_percent_fee_token: +bitmart_taker_fixed_fees: +bitmart_taker_percent_fee: +btc_markets_percent_fee_token: +btc_markets_maker_percent_fee: +btc_markets_taker_percent_fee: +btc_markets_buy_percent_fee_deducted_from_returns: +bybit_perpetual_buy_percent_fee_deducted_from_returns: +bybit_perpetual_maker_fixed_fees: +bybit_perpetual_maker_percent_fee: +bybit_perpetual_percent_fee_token: +bybit_perpetual_taker_fixed_fees: +bybit_perpetual_taker_percent_fee: +bybit_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bybit_perpetual_testnet_maker_fixed_fees: +bybit_perpetual_testnet_maker_percent_fee: +bybit_perpetual_testnet_percent_fee_token: +bybit_perpetual_testnet_taker_fixed_fees: +bybit_perpetual_testnet_taker_percent_fee: +coinbase_pro_buy_percent_fee_deducted_from_returns: +coinbase_pro_maker_fixed_fees: +coinbase_pro_maker_percent_fee: +coinbase_pro_percent_fee_token: +coinbase_pro_taker_fixed_fees: +coinbase_pro_taker_percent_fee: +dydx_perpetual_buy_percent_fee_deducted_from_returns: +dydx_perpetual_maker_fixed_fees: +dydx_perpetual_maker_percent_fee: +dydx_perpetual_percent_fee_token: +dydx_perpetual_taker_fixed_fees: +dydx_perpetual_taker_percent_fee: +gate_io_buy_percent_fee_deducted_from_returns: +gate_io_maker_fixed_fees: +gate_io_maker_percent_fee: +gate_io_percent_fee_token: +gate_io_taker_fixed_fees: +gate_io_taker_percent_fee: +hitbtc_buy_percent_fee_deducted_from_returns: +hitbtc_maker_fixed_fees: +hitbtc_maker_percent_fee: +hitbtc_percent_fee_token: +hitbtc_taker_fixed_fees: +hitbtc_taker_percent_fee: +huobi_buy_percent_fee_deducted_from_returns: +huobi_maker_fixed_fees: +huobi_maker_percent_fee: +huobi_percent_fee_token: +huobi_taker_fixed_fees: +huobi_taker_percent_fee: +kraken_buy_percent_fee_deducted_from_returns: +kraken_maker_fixed_fees: +kraken_maker_percent_fee: +kraken_percent_fee_token: +kraken_taker_fixed_fees: +kraken_taker_percent_fee: +kucoin_buy_percent_fee_deducted_from_returns: +kucoin_maker_fixed_fees: +kucoin_maker_percent_fee: +kucoin_percent_fee_token: +kucoin_taker_fixed_fees: +kucoin_taker_percent_fee: +mexc_buy_percent_fee_deducted_from_returns: +mexc_maker_fixed_fees: +mexc_maker_percent_fee: +mexc_percent_fee_token: +mexc_taker_fixed_fees: +mexc_taker_percent_fee: +ndax_buy_percent_fee_deducted_from_returns: +ndax_maker_fixed_fees: +ndax_maker_percent_fee: +ndax_percent_fee_token: +ndax_taker_fixed_fees: +ndax_taker_percent_fee: +ndax_testnet_buy_percent_fee_deducted_from_returns: +ndax_testnet_maker_fixed_fees: +ndax_testnet_maker_percent_fee: +ndax_testnet_percent_fee_token: +ndax_testnet_taker_fixed_fees: +ndax_testnet_taker_percent_fee: +okx_buy_percent_fee_deducted_from_returns: +okx_maker_fixed_fees: +okx_maker_percent_fee: +okx_percent_fee_token: +okx_taker_fixed_fees: +okx_taker_percent_fee: +phemex_perpetual_percent_fee_token: +phemex_perpetual_maker_percent_fee: +phemex_perpetual_taker_percent_fee: +phemex_perpetual_buy_percent_fee_deducted_from_returns: +phemex_perpetual_maker_fixed_fees: +phemex_perpetual_taker_fixed_fees: +phemex_perpetual_testnet_percent_fee_token: +phemex_perpetual_testnet_maker_percent_fee: +phemex_perpetual_testnet_taker_percent_fee: +phemex_perpetual_testnet_buy_percent_fee_deducted_from_returns: +phemex_perpetual_testnet_maker_fixed_fees: +phemex_perpetual_testnet_taker_fixed_fees: +injective_v2_perpetual_percent_fee_token: +injective_v2_perpetual_maker_percent_fee: +injective_v2_perpetual_taker_percent_fee: +injective_v2_perpetual_buy_percent_fee_deducted_from_returns: +injective_v2_perpetual_maker_fixed_fees: +injective_v2_perpetual_taker_fixed_fees: +bitget_perpetual_percent_fee_token: +bitget_perpetual_maker_percent_fee: +bitget_perpetual_taker_percent_fee: +bitget_perpetual_buy_percent_fee_deducted_from_returns: +bitget_perpetual_maker_fixed_fees: +bitget_perpetual_taker_fixed_fees: +bit_com_perpetual_percent_fee_token: +bit_com_perpetual_maker_percent_fee: +bit_com_perpetual_taker_percent_fee: +bit_com_perpetual_buy_percent_fee_deducted_from_returns: +bit_com_perpetual_maker_fixed_fees: +bit_com_perpetual_taker_fixed_fees: +bit_com_perpetual_testnet_percent_fee_token: +bit_com_perpetual_testnet_maker_percent_fee: +bit_com_perpetual_testnet_taker_percent_fee: +bit_com_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bit_com_perpetual_testnet_maker_fixed_fees: +bit_com_perpetual_testnet_taker_fixed_fees: +gate_io_perpetual_percent_fee_token: +gate_io_perpetual_maker_percent_fee: +gate_io_perpetual_taker_percent_fee: +gate_io_perpetual_buy_percent_fee_deducted_from_returns: +gate_io_perpetual_maker_fixed_fees: +gate_io_perpetual_taker_fixed_fees: +bitmex_perpetual_percent_fee_token: +bitmex_perpetual_maker_percent_fee: +bitmex_perpetual_taker_percent_fee: +bitmex_perpetual_buy_percent_fee_deducted_from_returns: +bitmex_perpetual_maker_fixed_fees: +bitmex_perpetual_taker_fixed_fees: +bitmex_perpetual_testnet_percent_fee_token: +bitmex_perpetual_testnet_maker_percent_fee: +bitmex_perpetual_testnet_taker_percent_fee: +bitmex_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bitmex_perpetual_testnet_maker_fixed_fees: +bitmex_perpetual_testnet_taker_fixed_fees: +kucoin_perpetual_percent_fee_token: +kucoin_perpetual_maker_percent_fee: +kucoin_perpetual_taker_percent_fee: +kucoin_perpetual_buy_percent_fee_deducted_from_returns: +kucoin_perpetual_maker_fixed_fees: +kucoin_perpetual_taker_fixed_fees: +bitmex_percent_fee_token: +bitmex_maker_percent_fee: +bitmex_taker_percent_fee: +bitmex_buy_percent_fee_deducted_from_returns: +bitmex_maker_fixed_fees: +bitmex_taker_fixed_fees: +bitmex_testnet_percent_fee_token: +bitmex_testnet_maker_percent_fee: +bitmex_testnet_taker_percent_fee: +bitmex_testnet_buy_percent_fee_deducted_from_returns: +bitmex_testnet_maker_fixed_fees: +bitmex_testnet_taker_fixed_fees: +btc_markets_maker_fixed_fees: +btc_markets_taker_fixed_fees: +woo_x_percent_fee_token: +woo_x_maker_percent_fee: +woo_x_taker_percent_fee: +woo_x_buy_percent_fee_deducted_from_returns: +woo_x_maker_fixed_fees: +woo_x_taker_fixed_fees: +woo_x_testnet_percent_fee_token: +woo_x_testnet_maker_percent_fee: +woo_x_testnet_taker_percent_fee: +woo_x_testnet_buy_percent_fee_deducted_from_returns: +woo_x_testnet_maker_fixed_fees: +woo_x_testnet_taker_fixed_fees: +polkadex_percent_fee_token: +polkadex_maker_percent_fee: +polkadex_taker_percent_fee: +polkadex_buy_percent_fee_deducted_from_returns: +polkadex_maker_fixed_fees: +polkadex_taker_fixed_fees: +bybit_percent_fee_token: +bybit_maker_percent_fee: +bybit_taker_percent_fee: +bybit_buy_percent_fee_deducted_from_returns: +bybit_maker_fixed_fees: +bybit_taker_fixed_fees: +bybit_testnet_percent_fee_token: +bybit_testnet_maker_percent_fee: +bybit_testnet_taker_percent_fee: +bybit_testnet_buy_percent_fee_deducted_from_returns: +bybit_testnet_maker_fixed_fees: +bybit_testnet_taker_fixed_fees: +injective_v2_percent_fee_token: +injective_v2_maker_percent_fee: +injective_v2_taker_percent_fee: +injective_v2_buy_percent_fee_deducted_from_returns: +injective_v2_maker_fixed_fees: +injective_v2_taker_fixed_fees: +vertex_percent_fee_token: +vertex_maker_percent_fee: +vertex_taker_percent_fee: +vertex_buy_percent_fee_deducted_from_returns: +vertex_maker_fixed_fees: +vertex_taker_fixed_fees: +vertex_testnet_percent_fee_token: +vertex_testnet_maker_percent_fee: +vertex_testnet_taker_percent_fee: +vertex_testnet_buy_percent_fee_deducted_from_returns: +vertex_testnet_maker_fixed_fees: +vertex_testnet_taker_fixed_fees: +mock_paper_exchange_percent_fee_token: +mock_paper_exchange_maker_percent_fee: +mock_paper_exchange_taker_percent_fee: +mock_paper_exchange_buy_percent_fee_deducted_from_returns: +mock_paper_exchange_maker_fixed_fees: +mock_paper_exchange_taker_fixed_fees: +kucoin_perpetual_testnet_percent_fee_token: +kucoin_perpetual_testnet_maker_percent_fee: +kucoin_perpetual_testnet_taker_percent_fee: +kucoin_perpetual_testnet_buy_percent_fee_deducted_from_returns: +kucoin_perpetual_testnet_maker_fixed_fees: +kucoin_perpetual_testnet_taker_fixed_fees: +foxbit_percent_fee_token: +foxbit_maker_percent_fee: +foxbit_taker_percent_fee: +foxbit_buy_percent_fee_deducted_from_returns: +foxbit_maker_fixed_fees: +foxbit_taker_fixed_fees: +hyperliquid_perpetual_percent_fee_token: +hyperliquid_perpetual_maker_percent_fee: +hyperliquid_perpetual_taker_percent_fee: +hyperliquid_perpetual_buy_percent_fee_deducted_from_returns: +hyperliquid_perpetual_maker_fixed_fees: +hyperliquid_perpetual_taker_fixed_fees: +hyperliquid_perpetual_testnet_percent_fee_token: +hyperliquid_perpetual_testnet_maker_percent_fee: +hyperliquid_perpetual_testnet_taker_percent_fee: +hyperliquid_perpetual_testnet_buy_percent_fee_deducted_from_returns: +hyperliquid_perpetual_testnet_maker_fixed_fees: +hyperliquid_perpetual_testnet_taker_fixed_fees: +bitmart_perpetual_percent_fee_token: +bitmart_perpetual_maker_percent_fee: +bitmart_perpetual_taker_percent_fee: +bitmart_perpetual_buy_percent_fee_deducted_from_returns: +bitmart_perpetual_maker_fixed_fees: +bitmart_perpetual_taker_fixed_fees: +architect_perpetual_percent_fee_token: +architect_perpetual_maker_percent_fee: +architect_perpetual_taker_percent_fee: +architect_perpetual_buy_percent_fee_deducted_from_returns: +architect_perpetual_maker_fixed_fees: +architect_perpetual_taker_fixed_fees: +architect_perpetual_sandbox_percent_fee_token: +architect_perpetual_sandbox_maker_percent_fee: +architect_perpetual_sandbox_taker_percent_fee: +architect_perpetual_sandbox_buy_percent_fee_deducted_from_returns: +architect_perpetual_sandbox_maker_fixed_fees: +architect_perpetual_sandbox_taker_fixed_fees: +pacifica_perpetual_percent_fee_token: +pacifica_perpetual_maker_percent_fee: +pacifica_perpetual_taker_percent_fee: +pacifica_perpetual_buy_percent_fee_deducted_from_returns: +pacifica_perpetual_maker_fixed_fees: +pacifica_perpetual_taker_fixed_fees: +pacifica_perpetual_testnet_percent_fee_token: +pacifica_perpetual_testnet_maker_percent_fee: +pacifica_perpetual_testnet_taker_percent_fee: +pacifica_perpetual_testnet_buy_percent_fee_deducted_from_returns: +pacifica_perpetual_testnet_maker_fixed_fees: +pacifica_perpetual_testnet_taker_fixed_fees: +lighter_perpetual_percent_fee_token: +lighter_perpetual_maker_percent_fee: +lighter_perpetual_taker_percent_fee: +lighter_perpetual_buy_percent_fee_deducted_from_returns: +lighter_perpetual_maker_fixed_fees: +lighter_perpetual_taker_fixed_fees: +lighter_perpetual_testnet_percent_fee_token: +lighter_perpetual_testnet_maker_percent_fee: +lighter_perpetual_testnet_taker_percent_fee: +lighter_perpetual_testnet_buy_percent_fee_deducted_from_returns: +lighter_perpetual_testnet_maker_fixed_fees: +lighter_perpetual_testnet_taker_fixed_fees: +derive_perpetual_percent_fee_token: +derive_perpetual_maker_percent_fee: +derive_perpetual_taker_percent_fee: +derive_perpetual_buy_percent_fee_deducted_from_returns: +derive_perpetual_maker_fixed_fees: +derive_perpetual_taker_fixed_fees: +derive_perpetual_testnet_percent_fee_token: +derive_perpetual_testnet_maker_percent_fee: +derive_perpetual_testnet_taker_percent_fee: +derive_perpetual_testnet_buy_percent_fee_deducted_from_returns: +derive_perpetual_testnet_maker_fixed_fees: +derive_perpetual_testnet_taker_fixed_fees: +aevo_perpetual_percent_fee_token: +aevo_perpetual_maker_percent_fee: +aevo_perpetual_taker_percent_fee: +aevo_perpetual_buy_percent_fee_deducted_from_returns: +aevo_perpetual_maker_fixed_fees: +aevo_perpetual_taker_fixed_fees: +aevo_perpetual_testnet_percent_fee_token: +aevo_perpetual_testnet_maker_percent_fee: +aevo_perpetual_testnet_taker_percent_fee: +aevo_perpetual_testnet_buy_percent_fee_deducted_from_returns: +aevo_perpetual_testnet_maker_fixed_fees: +aevo_perpetual_testnet_taker_fixed_fees: +dydx_v4_perpetual_percent_fee_token: +dydx_v4_perpetual_maker_percent_fee: +dydx_v4_perpetual_taker_percent_fee: +dydx_v4_perpetual_buy_percent_fee_deducted_from_returns: +dydx_v4_perpetual_maker_fixed_fees: +dydx_v4_perpetual_taker_fixed_fees: +okx_perpetual_percent_fee_token: +okx_perpetual_maker_percent_fee: +okx_perpetual_taker_percent_fee: +okx_perpetual_buy_percent_fee_deducted_from_returns: +okx_perpetual_maker_fixed_fees: +okx_perpetual_taker_fixed_fees: +grvt_perpetual_percent_fee_token: +grvt_perpetual_maker_percent_fee: +grvt_perpetual_taker_percent_fee: +grvt_perpetual_buy_percent_fee_deducted_from_returns: +grvt_perpetual_maker_fixed_fees: +grvt_perpetual_taker_fixed_fees: +grvt_perpetual_testnet_percent_fee_token: +grvt_perpetual_testnet_maker_percent_fee: +grvt_perpetual_testnet_taker_percent_fee: +grvt_perpetual_testnet_buy_percent_fee_deducted_from_returns: +grvt_perpetual_testnet_maker_fixed_fees: +grvt_perpetual_testnet_taker_fixed_fees: +evedex_perpetual_percent_fee_token: +evedex_perpetual_maker_percent_fee: +evedex_perpetual_taker_percent_fee: +evedex_perpetual_buy_percent_fee_deducted_from_returns: +evedex_perpetual_maker_fixed_fees: +evedex_perpetual_taker_fixed_fees: +backpack_perpetual_percent_fee_token: +backpack_perpetual_maker_percent_fee: +backpack_perpetual_taker_percent_fee: +backpack_perpetual_buy_percent_fee_deducted_from_returns: +backpack_perpetual_maker_fixed_fees: +backpack_perpetual_taker_fixed_fees: +decibel_perpetual_percent_fee_token: +decibel_perpetual_maker_percent_fee: +decibel_perpetual_taker_percent_fee: +decibel_perpetual_buy_percent_fee_deducted_from_returns: +decibel_perpetual_maker_fixed_fees: +decibel_perpetual_taker_fixed_fees: +decibel_perpetual_testnet_percent_fee_token: +decibel_perpetual_testnet_maker_percent_fee: +decibel_perpetual_testnet_taker_percent_fee: +decibel_perpetual_testnet_buy_percent_fee_deducted_from_returns: +decibel_perpetual_testnet_maker_fixed_fees: +decibel_perpetual_testnet_taker_fixed_fees: +derive_percent_fee_token: +derive_maker_percent_fee: +derive_taker_percent_fee: +derive_buy_percent_fee_deducted_from_returns: +derive_maker_fixed_fees: +derive_taker_fixed_fees: +derive_testnet_percent_fee_token: +derive_testnet_maker_percent_fee: +derive_testnet_taker_percent_fee: +derive_testnet_buy_percent_fee_deducted_from_returns: +derive_testnet_maker_fixed_fees: +derive_testnet_taker_fixed_fees: +bitrue_percent_fee_token: +bitrue_maker_percent_fee: +bitrue_taker_percent_fee: +bitrue_buy_percent_fee_deducted_from_returns: +bitrue_maker_fixed_fees: +bitrue_taker_fixed_fees: +lambdaplex_percent_fee_token: +lambdaplex_maker_percent_fee: +lambdaplex_taker_percent_fee: +lambdaplex_buy_percent_fee_deducted_from_returns: +lambdaplex_maker_fixed_fees: +lambdaplex_taker_fixed_fees: +dexalot_percent_fee_token: +dexalot_maker_percent_fee: +dexalot_taker_percent_fee: +dexalot_buy_percent_fee_deducted_from_returns: +dexalot_maker_fixed_fees: +dexalot_taker_fixed_fees: +dexalot_testnet_percent_fee_token: +dexalot_testnet_maker_percent_fee: +dexalot_testnet_taker_percent_fee: +dexalot_testnet_buy_percent_fee_deducted_from_returns: +dexalot_testnet_maker_fixed_fees: +dexalot_testnet_taker_fixed_fees: +bing_x_percent_fee_token: +bing_x_maker_percent_fee: +bing_x_taker_percent_fee: +bing_x_buy_percent_fee_deducted_from_returns: +bing_x_maker_fixed_fees: +bing_x_taker_fixed_fees: +kucoin_hft_percent_fee_token: +kucoin_hft_maker_percent_fee: +kucoin_hft_taker_percent_fee: +kucoin_hft_buy_percent_fee_deducted_from_returns: +kucoin_hft_maker_fixed_fees: +kucoin_hft_taker_fixed_fees: +bitstamp_percent_fee_token: +bitstamp_maker_percent_fee: +bitstamp_taker_percent_fee: +bitstamp_buy_percent_fee_deducted_from_returns: +bitstamp_maker_fixed_fees: +bitstamp_taker_fixed_fees: +gemini_percent_fee_token: +gemini_maker_percent_fee: +gemini_taker_percent_fee: +gemini_buy_percent_fee_deducted_from_returns: +gemini_maker_fixed_fees: +gemini_taker_fixed_fees: +backpack_percent_fee_token: +backpack_maker_percent_fee: +backpack_taker_percent_fee: +backpack_buy_percent_fee_deducted_from_returns: +backpack_maker_fixed_fees: +backpack_taker_fixed_fees: +htx_percent_fee_token: +htx_maker_percent_fee: +htx_taker_percent_fee: +htx_buy_percent_fee_deducted_from_returns: +htx_maker_fixed_fees: +htx_taker_fixed_fees: +lighter_percent_fee_token: +lighter_maker_percent_fee: +lighter_taker_percent_fee: +lighter_buy_percent_fee_deducted_from_returns: +lighter_maker_fixed_fees: +lighter_taker_fixed_fees: +lighter_testnet_percent_fee_token: +lighter_testnet_maker_percent_fee: +lighter_testnet_taker_percent_fee: +lighter_testnet_buy_percent_fee_deducted_from_returns: +lighter_testnet_maker_fixed_fees: +lighter_testnet_taker_fixed_fees: +hyperliquid_percent_fee_token: +hyperliquid_maker_percent_fee: +hyperliquid_taker_percent_fee: +hyperliquid_buy_percent_fee_deducted_from_returns: +hyperliquid_maker_fixed_fees: +hyperliquid_taker_fixed_fees: +hyperliquid_testnet_percent_fee_token: +hyperliquid_testnet_maker_percent_fee: +hyperliquid_testnet_taker_percent_fee: +hyperliquid_testnet_buy_percent_fee_deducted_from_returns: +hyperliquid_testnet_maker_fixed_fees: +hyperliquid_testnet_taker_fixed_fees: +coinbase_advanced_trade_percent_fee_token: +coinbase_advanced_trade_maker_percent_fee: +coinbase_advanced_trade_taker_percent_fee: +coinbase_advanced_trade_buy_percent_fee_deducted_from_returns: +coinbase_advanced_trade_maker_fixed_fees: +coinbase_advanced_trade_taker_fixed_fees: +xrpl_percent_fee_token: +xrpl_maker_percent_fee: +xrpl_taker_percent_fee: +xrpl_buy_percent_fee_deducted_from_returns: +xrpl_maker_fixed_fees: +xrpl_taker_fixed_fees: +bitget_percent_fee_token: +bitget_maker_percent_fee: +bitget_taker_percent_fee: +bitget_buy_percent_fee_deducted_from_returns: +bitget_maker_fixed_fees: +bitget_taker_fixed_fees: diff --git a/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/connectors/.dockerignore b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/connectors/.dockerignore new file mode 100644 index 00000000..ad30bfec --- /dev/null +++ b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/connectors/.dockerignore @@ -0,0 +1 @@ +*.yml \ No newline at end of file diff --git a/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/connectors/.gitignore b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/connectors/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/connectors/xrpl.yml b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/connectors/xrpl.yml new file mode 100644 index 00000000..c4f7a2a4 --- /dev/null +++ b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/connectors/xrpl.yml @@ -0,0 +1,20 @@ +####################### +### xrpl config ### +####################### + +connector: xrpl + +xrpl_secret_key: 7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20223231663134393764323839646334653336626339396166636430303635303534227d2c202263697068657274657874223a20226533343635353939656434393262653036336238363338303164343262373930303435393734353536386633656361386131646135363164313934643737222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20223061613133383663313339383462666261653937316239366361303239313639227d2c20226d6163223a202234613536643862613033646536623163316635326463303165373463653337373066636263396362303739303865333864343163346533356430633035363038227d2c202276657273696f6e223a20332c2022616c696173223a2022227d + +wss_node_urls: +- wss://alien-magical-sky.xrp-mainnet.quiknode.pro/cdeda141d5c67711f061b73b1c4bf70eb163c6a8/ + +custom_markets: + SOLO-XRP: + base: SOLO + quote: XRP + base_issuer: rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz + quote_issuer: '' + trading_pair_symbol: null + +max_request_per_minute: 60 diff --git a/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/controllers/001_pmm_xrpl_USDC-XRP.yml b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/controllers/001_pmm_xrpl_USDC-XRP.yml new file mode 100644 index 00000000..8ca91e37 --- /dev/null +++ b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/controllers/001_pmm_xrpl_USDC-XRP.yml @@ -0,0 +1,37 @@ +buy_amounts_pct: 1,1 +buy_cooldown_time: 60 +buy_position_effectivization_time: 300 +buy_spreads: 0.003,0.007 +connector_name: xrpl +controller_name: pmm_mister +controller_type: generic +executor_refresh_time: 60 +global_pnl_reference: position +global_sl_activation_from: target_base +global_sl_enabled: true +global_stop_loss: 0.05 +global_take_profit: 0.03 +global_tp_activation_from: min_base +global_tp_enabled: true +id: 001_pmm_xrpl_USDC-XRP +leverage: 1 +max_active_executors_by_level: 2 +max_base_pct: 0.8 +min_base_pct: 0.5 +min_skew: 2 +open_order_type: 3 +portfolio_allocation: 0.1 +position_profit_protection: true +price_distance_tolerance: 0.001 +refresh_tolerance: 0.001 +sell_amounts_pct: 1,1 +sell_cooldown_time: 300 +sell_position_effectivization_time: 300 +sell_spreads: 0.007,0.014 +take_profit: 0.0015 +take_profit_order_type: 3 +target_base_pct: 0.65 +tick_mode: false +tolerance_scaling: 1.3 +total_amount_quote: 50 +trading_pair: USDC-XRP diff --git a/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/hummingbot_logs.yml b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/hummingbot_logs.yml new file mode 100755 index 00000000..8e65271c --- /dev/null +++ b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/hummingbot_logs.yml @@ -0,0 +1,83 @@ +--- +version: 1 +template_version: 12 + +formatters: + simple: + format: "%(asctime)s - %(process)d - %(name)s - %(levelname)s - %(message)s" + +handlers: + console: + class: hummingbot.logger.cli_handler.CLIHandler + level: DEBUG + formatter: simple + stream: ext://sys.stdout + console_warning: + class: hummingbot.logger.cli_handler.CLIHandler + level: WARNING + formatter: simple + stream: ext://sys.stdout + console_info: + class: hummingbot.logger.cli_handler.CLIHandler + level: INFO + formatter: simple + stream: ext://sys.stdout + file_handler: + class: logging.handlers.TimedRotatingFileHandler + level: DEBUG + formatter: simple + filename: $PROJECT_DIR/logs/logs_$STRATEGY_FILE_PATH.log + encoding: utf8 + when: "D" + interval: 1 + backupCount: 7 + "null": + class: logging.NullHandler + level: DEBUG + +loggers: + hummingbot.core.utils.eth_gas_station_lookup: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.logger.log_server_client: + level: WARNING + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.logger.reporting_proxy_handler: + level: WARNING + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.strategy: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.connector: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.client: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.core.event.event_reporter: + level: EVENT_LOG + propagate: false + handlers: [file_handler] + mqtt: false + conf: + level: NETWORK + handlers: ["null"] + propagate: false + mqtt: false + +root: + level: INFO + handlers: [console, file_handler] + mqtt: true diff --git a/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/scripts/xrpl-pmm-usdc-xrp-20260807-171512.yml b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/scripts/xrpl-pmm-usdc-xrp-20260807-171512.yml new file mode 100644 index 00000000..67c21f58 --- /dev/null +++ b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/conf/scripts/xrpl-pmm-usdc-xrp-20260807-171512.yml @@ -0,0 +1,3 @@ +controllers_config: +- 001_pmm_xrpl_USDC-XRP.yml +script_file_name: v2_with_controllers.py diff --git a/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/data/xrpl-pmm-usdc-xrp-20260807-171512.sqlite b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/data/xrpl-pmm-usdc-xrp-20260807-171512.sqlite new file mode 100644 index 00000000..ec005b97 Binary files /dev/null and b/bots/archived/xrpl-pmm-usdc-xrp-20260807-171512/data/xrpl-pmm-usdc-xrp-20260807-171512.sqlite differ diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/.password_verification b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/.password_verification new file mode 100644 index 00000000..d1a274b6 --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/.password_verification @@ -0,0 +1 @@ +7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20226463613937613132666365383432623664323437613830393966313638373662227d2c202263697068657274657874223a20223537393331373032636666343037396439623430222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226536376664366338373033386336613439653066373938316366656138656539227d2c20226d6163223a202233366134323163646334303464393433343264656166613037623235373866363734383930653961393765303037393231333132326532333464303666323034227d2c202276657273696f6e223a20332c2022616c696173223a2022227d \ No newline at end of file diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/conf_client.yml b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/conf_client.yml new file mode 100644 index 00000000..0d034bf7 --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/conf_client.yml @@ -0,0 +1,179 @@ +#################################### +### client_config_map config ### +#################################### + +instance_id: xrpl-xrp-usdc-mm-20260807-173803 + +# Fetch trading pairs from all exchanges if True, otherwise fetch only from connected exchanges. +fetch_pairs_from_all_exchanges: false + +log_level: INFO + +debug_console: false + +strategy_report_interval: 900.0 + +logger_override_whitelist: +- hummingbot.strategy.arbitrage +- hummingbot.strategy.cross_exchange_market_making +- conf + +log_file_path: /home/hummingbot/logs + +kill_switch_mode: {} + +# What to auto-fill in the prompt after each import command (start/config) +autofill_import: disabled + +# MQTT Bridge configuration. +mqtt_bridge: + mqtt_host: localhost + mqtt_port: 1883 + mqtt_username: '' + mqtt_password: '' + mqtt_namespace: hbot + mqtt_ssl: false + mqtt_logger: true + mqtt_notifier: true + mqtt_commands: true + mqtt_events: true + mqtt_external_events: true + mqtt_autostart: true + +# Error log sharing +send_error_logs: true + +# Advanced database options, currently supports SQLAlchemy's included dialects +# Reference: https://docs.sqlalchemy.org/en/13/dialects/ +# To use an instance of SQLite DB the required configuration is +# db_engine: sqlite +# To use a DBMS the required configuration is +# db_host: 127.0.0.1 +# db_port: 3306 +# db_username: username +# db_password: password +# db_name: dbname +db_mode: + db_engine: sqlite + +# Balance Limit Configurations +# e.g. Setting USDT and BTC limits on Binance. +# balance_asset_limit: +# binance: +# BTC: 0.1 +# USDT: 1000 +balance_asset_limit: + ascend_ex: {} + ascend_ex_paper_trade: {} + binance: {} + binance_paper_trade: {} + binance_us: {} + bitfinex: {} + bitmart: {} + bitmex: {} + bitmex_testnet: {} + btc_markets: {} + bybit: {} + bybit_testnet: {} + coinbase_pro: {} + foxbit: {} + gate_io: {} + gate_io_paper_trade: {} + hitbtc: {} + huobi: {} + injective_v2: {} + kraken: {} + kucoin: {} + kucoin_paper_trade: {} + mexc: {} + mock_paper_exchange: {} + ndax: {} + ndax_testnet: {} + okx: {} + polkadex: {} + vertex: {} + vertex_testnet: {} + woo_x: {} + woo_x_testnet: {} + +# Fixed gas price (in Gwei) for Ethereum transactions +manual_gas_price: 50.0 + +# Gateway API Configurations +# default host to only use localhost +# Port need to match the final installation port for Gateway +gateway: + gateway_api_host: localhost + gateway_api_port: '15888' + gateway_use_ssl: true + +# Whether to enable aggregated order and trade data collection +anonymized_metrics_mode: + anonymized_metrics_interval_min: 15.0 + +# A source for rate oracle, currently binance, coin_gecko, coin_cap, dexalot, evedex_perpetual, decibel_perpetual, kucoin, gate_io, backpack, coinbase_advanced_trade, hyperliquid, hyperliquid_perpetual, architect_perpetual, derive, mexc +rate_oracle_source: + name: gate_io + +# A universal token which to display tokens values in, e.g. USD,EUR,BTC +global_token: + global_token_name: USDT + global_token_symbol: $ + usd_equivalent_tokens: + - USD + +# Percentage of API rate limits (on any exchange and any end point) allocated to this bot instance. +# Enter 50 to indicate 50%. E.g. if the API rate limit is 100 calls per second, and you allocate +# 50% to this setting, the bot will have a maximum (limit) of 50 calls per second +rate_limits_share_pct: 100.0 + +commands_timeout: + create_command_timeout: 10.0 + other_commands_timeout: 30.0 + +# Tabulate table format style (https://github.com/astanin/python-tabulate#table-format) +tables_format: psql + +paper_trade: + paper_trade_exchanges: + - binance + - kucoin + - ascend_ex + - gate_io + paper_trade_account_balance: + BTC: 1.0 + DAI: 1000.0 + ETH: 10.0 + ONE: 1000.0 + TUSD: 1000.0 + USDC: 1000.0 + USDQ: 1000.0 + USDT: 1000.0 + WETH: 10.0 + +color: + top_pane: '#000000' + bottom_pane: '#000000' + output_pane: '#262626' + input_pane: '#1C1C1C' + logs_pane: '#121212' + terminal_primary: '#5FFFD7' + primary_label: '#5FFFD7' + secondary_label: '#FFFFFF' + success_label: '#5FFFD7' + warning_label: '#FFFF00' + info_label: '#5FD7FF' + error_label: '#FF0000' + gold_label: '#FFD700' + silver_label: '#C0C0C0' + bronze_label: '#CD7F32' + +# The tick size is the frequency with which the clock notifies the time iterators by calling the +# c_tick() method, that means for example that if the tick size is 1, the logic of the strategy +# will run every second. +tick_size: 1.0 + +market_data_collection: + market_data_collection_enabled: false + market_data_collection_interval: 60 + market_data_collection_depth: 20 diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/conf_fee_overrides.yml b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/conf_fee_overrides.yml new file mode 100644 index 00000000..8dc9d57e --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/conf_fee_overrides.yml @@ -0,0 +1,526 @@ +######################################## +### Fee overrides configurations ### +######################################## + +# For more detailed information: https://docs.hummingbot.io +template_version: 14 + +# Example of the fields that can be specified to override the `TradeFeeFactory` default settings. +# If the field is missing or the value is left blank, the default value will be used. +# The percentage values are specified as 0.1 for 0.1%. +# +# [exchange name]_percent_fee_token: +# [exchange name]_maker_percent_fee: +# [exchange name]_taker_percent_fee: +# [exchange name]_buy_percent_fee_deducted_from_returns: # if False, the buy fee is added to the order costs +# [exchange name]_maker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]]) +# [exchange name]_taker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]]) + +binance_percent_fee_token: # BNB +binance_maker_percent_fee: # 0.75 +binance_taker_percent_fee: # 0.75 +binance_buy_percent_fee_deducted_from_returns: # True + +# List of supported Exchanges for which the user's conf/conf_fee_override.yml +# will work. This file currently needs to be in sync with hummingbot list of +# supported exchanges +ascend_ex_buy_percent_fee_deducted_from_returns: +ascend_ex_maker_fixed_fees: +ascend_ex_maker_percent_fee: +ascend_ex_percent_fee_token: +ascend_ex_taker_fixed_fees: +ascend_ex_taker_percent_fee: +binance_maker_fixed_fees: +binance_perpetual_buy_percent_fee_deducted_from_returns: +binance_perpetual_maker_fixed_fees: +binance_perpetual_maker_percent_fee: +binance_perpetual_percent_fee_token: +binance_perpetual_taker_fixed_fees: +binance_perpetual_taker_percent_fee: +binance_perpetual_testnet_buy_percent_fee_deducted_from_returns: +binance_perpetual_testnet_maker_fixed_fees: +binance_perpetual_testnet_maker_percent_fee: +binance_perpetual_testnet_percent_fee_token: +binance_perpetual_testnet_taker_fixed_fees: +binance_perpetual_testnet_taker_percent_fee: +binance_taker_fixed_fees: +binance_us_buy_percent_fee_deducted_from_returns: +binance_us_maker_fixed_fees: +binance_us_maker_percent_fee: +binance_us_percent_fee_token: +binance_us_taker_fixed_fees: +binance_us_taker_percent_fee: +bitfinex_buy_percent_fee_deducted_from_returns: +bitfinex_maker_fixed_fees: +bitfinex_maker_percent_fee: +bitfinex_percent_fee_token: +bitfinex_taker_fixed_fees: +bitfinex_taker_percent_fee: +bitmart_buy_percent_fee_deducted_from_returns: +bitmart_maker_fixed_fees: +bitmart_maker_percent_fee: +bitmart_percent_fee_token: +bitmart_taker_fixed_fees: +bitmart_taker_percent_fee: +btc_markets_percent_fee_token: +btc_markets_maker_percent_fee: +btc_markets_taker_percent_fee: +btc_markets_buy_percent_fee_deducted_from_returns: +bybit_perpetual_buy_percent_fee_deducted_from_returns: +bybit_perpetual_maker_fixed_fees: +bybit_perpetual_maker_percent_fee: +bybit_perpetual_percent_fee_token: +bybit_perpetual_taker_fixed_fees: +bybit_perpetual_taker_percent_fee: +bybit_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bybit_perpetual_testnet_maker_fixed_fees: +bybit_perpetual_testnet_maker_percent_fee: +bybit_perpetual_testnet_percent_fee_token: +bybit_perpetual_testnet_taker_fixed_fees: +bybit_perpetual_testnet_taker_percent_fee: +coinbase_pro_buy_percent_fee_deducted_from_returns: +coinbase_pro_maker_fixed_fees: +coinbase_pro_maker_percent_fee: +coinbase_pro_percent_fee_token: +coinbase_pro_taker_fixed_fees: +coinbase_pro_taker_percent_fee: +dydx_perpetual_buy_percent_fee_deducted_from_returns: +dydx_perpetual_maker_fixed_fees: +dydx_perpetual_maker_percent_fee: +dydx_perpetual_percent_fee_token: +dydx_perpetual_taker_fixed_fees: +dydx_perpetual_taker_percent_fee: +gate_io_buy_percent_fee_deducted_from_returns: +gate_io_maker_fixed_fees: +gate_io_maker_percent_fee: +gate_io_percent_fee_token: +gate_io_taker_fixed_fees: +gate_io_taker_percent_fee: +hitbtc_buy_percent_fee_deducted_from_returns: +hitbtc_maker_fixed_fees: +hitbtc_maker_percent_fee: +hitbtc_percent_fee_token: +hitbtc_taker_fixed_fees: +hitbtc_taker_percent_fee: +huobi_buy_percent_fee_deducted_from_returns: +huobi_maker_fixed_fees: +huobi_maker_percent_fee: +huobi_percent_fee_token: +huobi_taker_fixed_fees: +huobi_taker_percent_fee: +kraken_buy_percent_fee_deducted_from_returns: +kraken_maker_fixed_fees: +kraken_maker_percent_fee: +kraken_percent_fee_token: +kraken_taker_fixed_fees: +kraken_taker_percent_fee: +kucoin_buy_percent_fee_deducted_from_returns: +kucoin_maker_fixed_fees: +kucoin_maker_percent_fee: +kucoin_percent_fee_token: +kucoin_taker_fixed_fees: +kucoin_taker_percent_fee: +mexc_buy_percent_fee_deducted_from_returns: +mexc_maker_fixed_fees: +mexc_maker_percent_fee: +mexc_percent_fee_token: +mexc_taker_fixed_fees: +mexc_taker_percent_fee: +ndax_buy_percent_fee_deducted_from_returns: +ndax_maker_fixed_fees: +ndax_maker_percent_fee: +ndax_percent_fee_token: +ndax_taker_fixed_fees: +ndax_taker_percent_fee: +ndax_testnet_buy_percent_fee_deducted_from_returns: +ndax_testnet_maker_fixed_fees: +ndax_testnet_maker_percent_fee: +ndax_testnet_percent_fee_token: +ndax_testnet_taker_fixed_fees: +ndax_testnet_taker_percent_fee: +okx_buy_percent_fee_deducted_from_returns: +okx_maker_fixed_fees: +okx_maker_percent_fee: +okx_percent_fee_token: +okx_taker_fixed_fees: +okx_taker_percent_fee: +phemex_perpetual_percent_fee_token: +phemex_perpetual_maker_percent_fee: +phemex_perpetual_taker_percent_fee: +phemex_perpetual_buy_percent_fee_deducted_from_returns: +phemex_perpetual_maker_fixed_fees: +phemex_perpetual_taker_fixed_fees: +phemex_perpetual_testnet_percent_fee_token: +phemex_perpetual_testnet_maker_percent_fee: +phemex_perpetual_testnet_taker_percent_fee: +phemex_perpetual_testnet_buy_percent_fee_deducted_from_returns: +phemex_perpetual_testnet_maker_fixed_fees: +phemex_perpetual_testnet_taker_fixed_fees: +injective_v2_perpetual_percent_fee_token: +injective_v2_perpetual_maker_percent_fee: +injective_v2_perpetual_taker_percent_fee: +injective_v2_perpetual_buy_percent_fee_deducted_from_returns: +injective_v2_perpetual_maker_fixed_fees: +injective_v2_perpetual_taker_fixed_fees: +bitget_perpetual_percent_fee_token: +bitget_perpetual_maker_percent_fee: +bitget_perpetual_taker_percent_fee: +bitget_perpetual_buy_percent_fee_deducted_from_returns: +bitget_perpetual_maker_fixed_fees: +bitget_perpetual_taker_fixed_fees: +bit_com_perpetual_percent_fee_token: +bit_com_perpetual_maker_percent_fee: +bit_com_perpetual_taker_percent_fee: +bit_com_perpetual_buy_percent_fee_deducted_from_returns: +bit_com_perpetual_maker_fixed_fees: +bit_com_perpetual_taker_fixed_fees: +bit_com_perpetual_testnet_percent_fee_token: +bit_com_perpetual_testnet_maker_percent_fee: +bit_com_perpetual_testnet_taker_percent_fee: +bit_com_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bit_com_perpetual_testnet_maker_fixed_fees: +bit_com_perpetual_testnet_taker_fixed_fees: +gate_io_perpetual_percent_fee_token: +gate_io_perpetual_maker_percent_fee: +gate_io_perpetual_taker_percent_fee: +gate_io_perpetual_buy_percent_fee_deducted_from_returns: +gate_io_perpetual_maker_fixed_fees: +gate_io_perpetual_taker_fixed_fees: +bitmex_perpetual_percent_fee_token: +bitmex_perpetual_maker_percent_fee: +bitmex_perpetual_taker_percent_fee: +bitmex_perpetual_buy_percent_fee_deducted_from_returns: +bitmex_perpetual_maker_fixed_fees: +bitmex_perpetual_taker_fixed_fees: +bitmex_perpetual_testnet_percent_fee_token: +bitmex_perpetual_testnet_maker_percent_fee: +bitmex_perpetual_testnet_taker_percent_fee: +bitmex_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bitmex_perpetual_testnet_maker_fixed_fees: +bitmex_perpetual_testnet_taker_fixed_fees: +kucoin_perpetual_percent_fee_token: +kucoin_perpetual_maker_percent_fee: +kucoin_perpetual_taker_percent_fee: +kucoin_perpetual_buy_percent_fee_deducted_from_returns: +kucoin_perpetual_maker_fixed_fees: +kucoin_perpetual_taker_fixed_fees: +bitmex_percent_fee_token: +bitmex_maker_percent_fee: +bitmex_taker_percent_fee: +bitmex_buy_percent_fee_deducted_from_returns: +bitmex_maker_fixed_fees: +bitmex_taker_fixed_fees: +bitmex_testnet_percent_fee_token: +bitmex_testnet_maker_percent_fee: +bitmex_testnet_taker_percent_fee: +bitmex_testnet_buy_percent_fee_deducted_from_returns: +bitmex_testnet_maker_fixed_fees: +bitmex_testnet_taker_fixed_fees: +btc_markets_maker_fixed_fees: +btc_markets_taker_fixed_fees: +woo_x_percent_fee_token: +woo_x_maker_percent_fee: +woo_x_taker_percent_fee: +woo_x_buy_percent_fee_deducted_from_returns: +woo_x_maker_fixed_fees: +woo_x_taker_fixed_fees: +woo_x_testnet_percent_fee_token: +woo_x_testnet_maker_percent_fee: +woo_x_testnet_taker_percent_fee: +woo_x_testnet_buy_percent_fee_deducted_from_returns: +woo_x_testnet_maker_fixed_fees: +woo_x_testnet_taker_fixed_fees: +polkadex_percent_fee_token: +polkadex_maker_percent_fee: +polkadex_taker_percent_fee: +polkadex_buy_percent_fee_deducted_from_returns: +polkadex_maker_fixed_fees: +polkadex_taker_fixed_fees: +bybit_percent_fee_token: +bybit_maker_percent_fee: +bybit_taker_percent_fee: +bybit_buy_percent_fee_deducted_from_returns: +bybit_maker_fixed_fees: +bybit_taker_fixed_fees: +bybit_testnet_percent_fee_token: +bybit_testnet_maker_percent_fee: +bybit_testnet_taker_percent_fee: +bybit_testnet_buy_percent_fee_deducted_from_returns: +bybit_testnet_maker_fixed_fees: +bybit_testnet_taker_fixed_fees: +injective_v2_percent_fee_token: +injective_v2_maker_percent_fee: +injective_v2_taker_percent_fee: +injective_v2_buy_percent_fee_deducted_from_returns: +injective_v2_maker_fixed_fees: +injective_v2_taker_fixed_fees: +vertex_percent_fee_token: +vertex_maker_percent_fee: +vertex_taker_percent_fee: +vertex_buy_percent_fee_deducted_from_returns: +vertex_maker_fixed_fees: +vertex_taker_fixed_fees: +vertex_testnet_percent_fee_token: +vertex_testnet_maker_percent_fee: +vertex_testnet_taker_percent_fee: +vertex_testnet_buy_percent_fee_deducted_from_returns: +vertex_testnet_maker_fixed_fees: +vertex_testnet_taker_fixed_fees: +mock_paper_exchange_percent_fee_token: +mock_paper_exchange_maker_percent_fee: +mock_paper_exchange_taker_percent_fee: +mock_paper_exchange_buy_percent_fee_deducted_from_returns: +mock_paper_exchange_maker_fixed_fees: +mock_paper_exchange_taker_fixed_fees: +kucoin_perpetual_testnet_percent_fee_token: +kucoin_perpetual_testnet_maker_percent_fee: +kucoin_perpetual_testnet_taker_percent_fee: +kucoin_perpetual_testnet_buy_percent_fee_deducted_from_returns: +kucoin_perpetual_testnet_maker_fixed_fees: +kucoin_perpetual_testnet_taker_fixed_fees: +foxbit_percent_fee_token: +foxbit_maker_percent_fee: +foxbit_taker_percent_fee: +foxbit_buy_percent_fee_deducted_from_returns: +foxbit_maker_fixed_fees: +foxbit_taker_fixed_fees: +hyperliquid_perpetual_percent_fee_token: +hyperliquid_perpetual_maker_percent_fee: +hyperliquid_perpetual_taker_percent_fee: +hyperliquid_perpetual_buy_percent_fee_deducted_from_returns: +hyperliquid_perpetual_maker_fixed_fees: +hyperliquid_perpetual_taker_fixed_fees: +hyperliquid_perpetual_testnet_percent_fee_token: +hyperliquid_perpetual_testnet_maker_percent_fee: +hyperliquid_perpetual_testnet_taker_percent_fee: +hyperliquid_perpetual_testnet_buy_percent_fee_deducted_from_returns: +hyperliquid_perpetual_testnet_maker_fixed_fees: +hyperliquid_perpetual_testnet_taker_fixed_fees: +bitmart_perpetual_percent_fee_token: +bitmart_perpetual_maker_percent_fee: +bitmart_perpetual_taker_percent_fee: +bitmart_perpetual_buy_percent_fee_deducted_from_returns: +bitmart_perpetual_maker_fixed_fees: +bitmart_perpetual_taker_fixed_fees: +architect_perpetual_percent_fee_token: +architect_perpetual_maker_percent_fee: +architect_perpetual_taker_percent_fee: +architect_perpetual_buy_percent_fee_deducted_from_returns: +architect_perpetual_maker_fixed_fees: +architect_perpetual_taker_fixed_fees: +architect_perpetual_sandbox_percent_fee_token: +architect_perpetual_sandbox_maker_percent_fee: +architect_perpetual_sandbox_taker_percent_fee: +architect_perpetual_sandbox_buy_percent_fee_deducted_from_returns: +architect_perpetual_sandbox_maker_fixed_fees: +architect_perpetual_sandbox_taker_fixed_fees: +pacifica_perpetual_percent_fee_token: +pacifica_perpetual_maker_percent_fee: +pacifica_perpetual_taker_percent_fee: +pacifica_perpetual_buy_percent_fee_deducted_from_returns: +pacifica_perpetual_maker_fixed_fees: +pacifica_perpetual_taker_fixed_fees: +pacifica_perpetual_testnet_percent_fee_token: +pacifica_perpetual_testnet_maker_percent_fee: +pacifica_perpetual_testnet_taker_percent_fee: +pacifica_perpetual_testnet_buy_percent_fee_deducted_from_returns: +pacifica_perpetual_testnet_maker_fixed_fees: +pacifica_perpetual_testnet_taker_fixed_fees: +lighter_perpetual_percent_fee_token: +lighter_perpetual_maker_percent_fee: +lighter_perpetual_taker_percent_fee: +lighter_perpetual_buy_percent_fee_deducted_from_returns: +lighter_perpetual_maker_fixed_fees: +lighter_perpetual_taker_fixed_fees: +lighter_perpetual_testnet_percent_fee_token: +lighter_perpetual_testnet_maker_percent_fee: +lighter_perpetual_testnet_taker_percent_fee: +lighter_perpetual_testnet_buy_percent_fee_deducted_from_returns: +lighter_perpetual_testnet_maker_fixed_fees: +lighter_perpetual_testnet_taker_fixed_fees: +derive_perpetual_percent_fee_token: +derive_perpetual_maker_percent_fee: +derive_perpetual_taker_percent_fee: +derive_perpetual_buy_percent_fee_deducted_from_returns: +derive_perpetual_maker_fixed_fees: +derive_perpetual_taker_fixed_fees: +derive_perpetual_testnet_percent_fee_token: +derive_perpetual_testnet_maker_percent_fee: +derive_perpetual_testnet_taker_percent_fee: +derive_perpetual_testnet_buy_percent_fee_deducted_from_returns: +derive_perpetual_testnet_maker_fixed_fees: +derive_perpetual_testnet_taker_fixed_fees: +aevo_perpetual_percent_fee_token: +aevo_perpetual_maker_percent_fee: +aevo_perpetual_taker_percent_fee: +aevo_perpetual_buy_percent_fee_deducted_from_returns: +aevo_perpetual_maker_fixed_fees: +aevo_perpetual_taker_fixed_fees: +aevo_perpetual_testnet_percent_fee_token: +aevo_perpetual_testnet_maker_percent_fee: +aevo_perpetual_testnet_taker_percent_fee: +aevo_perpetual_testnet_buy_percent_fee_deducted_from_returns: +aevo_perpetual_testnet_maker_fixed_fees: +aevo_perpetual_testnet_taker_fixed_fees: +dydx_v4_perpetual_percent_fee_token: +dydx_v4_perpetual_maker_percent_fee: +dydx_v4_perpetual_taker_percent_fee: +dydx_v4_perpetual_buy_percent_fee_deducted_from_returns: +dydx_v4_perpetual_maker_fixed_fees: +dydx_v4_perpetual_taker_fixed_fees: +okx_perpetual_percent_fee_token: +okx_perpetual_maker_percent_fee: +okx_perpetual_taker_percent_fee: +okx_perpetual_buy_percent_fee_deducted_from_returns: +okx_perpetual_maker_fixed_fees: +okx_perpetual_taker_fixed_fees: +grvt_perpetual_percent_fee_token: +grvt_perpetual_maker_percent_fee: +grvt_perpetual_taker_percent_fee: +grvt_perpetual_buy_percent_fee_deducted_from_returns: +grvt_perpetual_maker_fixed_fees: +grvt_perpetual_taker_fixed_fees: +grvt_perpetual_testnet_percent_fee_token: +grvt_perpetual_testnet_maker_percent_fee: +grvt_perpetual_testnet_taker_percent_fee: +grvt_perpetual_testnet_buy_percent_fee_deducted_from_returns: +grvt_perpetual_testnet_maker_fixed_fees: +grvt_perpetual_testnet_taker_fixed_fees: +evedex_perpetual_percent_fee_token: +evedex_perpetual_maker_percent_fee: +evedex_perpetual_taker_percent_fee: +evedex_perpetual_buy_percent_fee_deducted_from_returns: +evedex_perpetual_maker_fixed_fees: +evedex_perpetual_taker_fixed_fees: +backpack_perpetual_percent_fee_token: +backpack_perpetual_maker_percent_fee: +backpack_perpetual_taker_percent_fee: +backpack_perpetual_buy_percent_fee_deducted_from_returns: +backpack_perpetual_maker_fixed_fees: +backpack_perpetual_taker_fixed_fees: +decibel_perpetual_percent_fee_token: +decibel_perpetual_maker_percent_fee: +decibel_perpetual_taker_percent_fee: +decibel_perpetual_buy_percent_fee_deducted_from_returns: +decibel_perpetual_maker_fixed_fees: +decibel_perpetual_taker_fixed_fees: +decibel_perpetual_testnet_percent_fee_token: +decibel_perpetual_testnet_maker_percent_fee: +decibel_perpetual_testnet_taker_percent_fee: +decibel_perpetual_testnet_buy_percent_fee_deducted_from_returns: +decibel_perpetual_testnet_maker_fixed_fees: +decibel_perpetual_testnet_taker_fixed_fees: +derive_percent_fee_token: +derive_maker_percent_fee: +derive_taker_percent_fee: +derive_buy_percent_fee_deducted_from_returns: +derive_maker_fixed_fees: +derive_taker_fixed_fees: +derive_testnet_percent_fee_token: +derive_testnet_maker_percent_fee: +derive_testnet_taker_percent_fee: +derive_testnet_buy_percent_fee_deducted_from_returns: +derive_testnet_maker_fixed_fees: +derive_testnet_taker_fixed_fees: +bitrue_percent_fee_token: +bitrue_maker_percent_fee: +bitrue_taker_percent_fee: +bitrue_buy_percent_fee_deducted_from_returns: +bitrue_maker_fixed_fees: +bitrue_taker_fixed_fees: +lambdaplex_percent_fee_token: +lambdaplex_maker_percent_fee: +lambdaplex_taker_percent_fee: +lambdaplex_buy_percent_fee_deducted_from_returns: +lambdaplex_maker_fixed_fees: +lambdaplex_taker_fixed_fees: +dexalot_percent_fee_token: +dexalot_maker_percent_fee: +dexalot_taker_percent_fee: +dexalot_buy_percent_fee_deducted_from_returns: +dexalot_maker_fixed_fees: +dexalot_taker_fixed_fees: +dexalot_testnet_percent_fee_token: +dexalot_testnet_maker_percent_fee: +dexalot_testnet_taker_percent_fee: +dexalot_testnet_buy_percent_fee_deducted_from_returns: +dexalot_testnet_maker_fixed_fees: +dexalot_testnet_taker_fixed_fees: +bing_x_percent_fee_token: +bing_x_maker_percent_fee: +bing_x_taker_percent_fee: +bing_x_buy_percent_fee_deducted_from_returns: +bing_x_maker_fixed_fees: +bing_x_taker_fixed_fees: +kucoin_hft_percent_fee_token: +kucoin_hft_maker_percent_fee: +kucoin_hft_taker_percent_fee: +kucoin_hft_buy_percent_fee_deducted_from_returns: +kucoin_hft_maker_fixed_fees: +kucoin_hft_taker_fixed_fees: +bitstamp_percent_fee_token: +bitstamp_maker_percent_fee: +bitstamp_taker_percent_fee: +bitstamp_buy_percent_fee_deducted_from_returns: +bitstamp_maker_fixed_fees: +bitstamp_taker_fixed_fees: +gemini_percent_fee_token: +gemini_maker_percent_fee: +gemini_taker_percent_fee: +gemini_buy_percent_fee_deducted_from_returns: +gemini_maker_fixed_fees: +gemini_taker_fixed_fees: +backpack_percent_fee_token: +backpack_maker_percent_fee: +backpack_taker_percent_fee: +backpack_buy_percent_fee_deducted_from_returns: +backpack_maker_fixed_fees: +backpack_taker_fixed_fees: +htx_percent_fee_token: +htx_maker_percent_fee: +htx_taker_percent_fee: +htx_buy_percent_fee_deducted_from_returns: +htx_maker_fixed_fees: +htx_taker_fixed_fees: +lighter_percent_fee_token: +lighter_maker_percent_fee: +lighter_taker_percent_fee: +lighter_buy_percent_fee_deducted_from_returns: +lighter_maker_fixed_fees: +lighter_taker_fixed_fees: +lighter_testnet_percent_fee_token: +lighter_testnet_maker_percent_fee: +lighter_testnet_taker_percent_fee: +lighter_testnet_buy_percent_fee_deducted_from_returns: +lighter_testnet_maker_fixed_fees: +lighter_testnet_taker_fixed_fees: +hyperliquid_percent_fee_token: +hyperliquid_maker_percent_fee: +hyperliquid_taker_percent_fee: +hyperliquid_buy_percent_fee_deducted_from_returns: +hyperliquid_maker_fixed_fees: +hyperliquid_taker_fixed_fees: +hyperliquid_testnet_percent_fee_token: +hyperliquid_testnet_maker_percent_fee: +hyperliquid_testnet_taker_percent_fee: +hyperliquid_testnet_buy_percent_fee_deducted_from_returns: +hyperliquid_testnet_maker_fixed_fees: +hyperliquid_testnet_taker_fixed_fees: +coinbase_advanced_trade_percent_fee_token: +coinbase_advanced_trade_maker_percent_fee: +coinbase_advanced_trade_taker_percent_fee: +coinbase_advanced_trade_buy_percent_fee_deducted_from_returns: +coinbase_advanced_trade_maker_fixed_fees: +coinbase_advanced_trade_taker_fixed_fees: +xrpl_percent_fee_token: +xrpl_maker_percent_fee: +xrpl_taker_percent_fee: +xrpl_buy_percent_fee_deducted_from_returns: +xrpl_maker_fixed_fees: +xrpl_taker_fixed_fees: +bitget_percent_fee_token: +bitget_maker_percent_fee: +bitget_taker_percent_fee: +bitget_buy_percent_fee_deducted_from_returns: +bitget_maker_fixed_fees: +bitget_taker_fixed_fees: diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/connectors/.dockerignore b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/connectors/.dockerignore new file mode 100644 index 00000000..ad30bfec --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/connectors/.dockerignore @@ -0,0 +1 @@ +*.yml \ No newline at end of file diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/connectors/.gitignore b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/connectors/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/connectors/xrpl.yml b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/connectors/xrpl.yml new file mode 100644 index 00000000..c4f7a2a4 --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/connectors/xrpl.yml @@ -0,0 +1,20 @@ +####################### +### xrpl config ### +####################### + +connector: xrpl + +xrpl_secret_key: 7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20223231663134393764323839646334653336626339396166636430303635303534227d2c202263697068657274657874223a20226533343635353939656434393262653036336238363338303164343262373930303435393734353536386633656361386131646135363164313934643737222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20223061613133383663313339383462666261653937316239366361303239313639227d2c20226d6163223a202234613536643862613033646536623163316635326463303165373463653337373066636263396362303739303865333864343163346533356430633035363038227d2c202276657273696f6e223a20332c2022616c696173223a2022227d + +wss_node_urls: +- wss://alien-magical-sky.xrp-mainnet.quiknode.pro/cdeda141d5c67711f061b73b1c4bf70eb163c6a8/ + +custom_markets: + SOLO-XRP: + base: SOLO + quote: XRP + base_issuer: rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz + quote_issuer: '' + trading_pair_symbol: null + +max_request_per_minute: 60 diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/controllers/xrpl_xrp_usdc_mm.yml b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/controllers/xrpl_xrp_usdc_mm.yml new file mode 100644 index 00000000..9347f724 --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/controllers/xrpl_xrp_usdc_mm.yml @@ -0,0 +1,39 @@ +buy_amounts_pct: 1,1 +buy_cooldown_time: 30 +buy_position_effectivization_time: 600 +buy_spreads: 0.0010,0.0020 +connector_name: xrpl +controller_name: pmm_mister +controller_type: generic +executor_refresh_time: 30 +global_pnl_reference: position +global_sl_activation_from: target_base +global_sl_enabled: true +global_stop_loss: 0.05 +global_take_profit: 0.03 +global_tp_activation_from: min_base +global_tp_enabled: true +id: xrpl_xrp_usdc_mm +leverage: 1 +manual_kill_switch: false +max_active_executors_by_level: 3 +max_base_pct: 0.7 +min_base_pct: 0.3 +min_skew: 1 +open_order_type: 3 +portfolio_allocation: 0.05 +position_mode: ONEWAY +position_profit_protection: true +price_distance_tolerance: 0.0005 +refresh_tolerance: 0.0005 +sell_amounts_pct: 1,1 +sell_cooldown_time: 30 +sell_position_effectivization_time: 600 +sell_spreads: 0.0015,0.0030 +take_profit: 0.0012 +take_profit_order_type: 3 +target_base_pct: 0.6 +tick_mode: false +tolerance_scaling: 1.2 +total_amount_quote: 100 +trading_pair: XRP-USDC diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/hummingbot_logs.yml b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/hummingbot_logs.yml new file mode 100755 index 00000000..8e65271c --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/hummingbot_logs.yml @@ -0,0 +1,83 @@ +--- +version: 1 +template_version: 12 + +formatters: + simple: + format: "%(asctime)s - %(process)d - %(name)s - %(levelname)s - %(message)s" + +handlers: + console: + class: hummingbot.logger.cli_handler.CLIHandler + level: DEBUG + formatter: simple + stream: ext://sys.stdout + console_warning: + class: hummingbot.logger.cli_handler.CLIHandler + level: WARNING + formatter: simple + stream: ext://sys.stdout + console_info: + class: hummingbot.logger.cli_handler.CLIHandler + level: INFO + formatter: simple + stream: ext://sys.stdout + file_handler: + class: logging.handlers.TimedRotatingFileHandler + level: DEBUG + formatter: simple + filename: $PROJECT_DIR/logs/logs_$STRATEGY_FILE_PATH.log + encoding: utf8 + when: "D" + interval: 1 + backupCount: 7 + "null": + class: logging.NullHandler + level: DEBUG + +loggers: + hummingbot.core.utils.eth_gas_station_lookup: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.logger.log_server_client: + level: WARNING + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.logger.reporting_proxy_handler: + level: WARNING + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.strategy: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.connector: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.client: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.core.event.event_reporter: + level: EVENT_LOG + propagate: false + handlers: [file_handler] + mqtt: false + conf: + level: NETWORK + handlers: ["null"] + propagate: false + mqtt: false + +root: + level: INFO + handlers: [console, file_handler] + mqtt: true diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/scripts/xrpl-xrp-usdc-mm-20260807-173803.yml b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/scripts/xrpl-xrp-usdc-mm-20260807-173803.yml new file mode 100644 index 00000000..4ec11c4d --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/conf/scripts/xrpl-xrp-usdc-mm-20260807-173803.yml @@ -0,0 +1,4 @@ +controllers_config: +- xrpl_xrp_usdc_mm.yml +max_global_drawdown_quote: 7.0 +script_file_name: v2_with_controllers.py diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/data/xrpl-xrp-usdc-mm-20260807-173803.sqlite b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/data/xrpl-xrp-usdc-mm-20260807-173803.sqlite new file mode 100644 index 00000000..dac98ee5 Binary files /dev/null and b/bots/archived/xrpl-xrp-usdc-mm-20260807-173803/data/xrpl-xrp-usdc-mm-20260807-173803.sqlite differ diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/.password_verification b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/.password_verification new file mode 100644 index 00000000..d1a274b6 --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/.password_verification @@ -0,0 +1 @@ +7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20226463613937613132666365383432623664323437613830393966313638373662227d2c202263697068657274657874223a20223537393331373032636666343037396439623430222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226536376664366338373033386336613439653066373938316366656138656539227d2c20226d6163223a202233366134323163646334303464393433343264656166613037623235373866363734383930653961393765303037393231333132326532333464303666323034227d2c202276657273696f6e223a20332c2022616c696173223a2022227d \ No newline at end of file diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/conf_client.yml b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/conf_client.yml new file mode 100644 index 00000000..ab8a282c --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/conf_client.yml @@ -0,0 +1,179 @@ +#################################### +### client_config_map config ### +#################################### + +instance_id: xrpl-xrp-usdc-mm-20260807-185151 + +# Fetch trading pairs from all exchanges if True, otherwise fetch only from connected exchanges. +fetch_pairs_from_all_exchanges: false + +log_level: INFO + +debug_console: false + +strategy_report_interval: 900.0 + +logger_override_whitelist: +- hummingbot.strategy.arbitrage +- hummingbot.strategy.cross_exchange_market_making +- conf + +log_file_path: /home/hummingbot/logs + +kill_switch_mode: {} + +# What to auto-fill in the prompt after each import command (start/config) +autofill_import: disabled + +# MQTT Bridge configuration. +mqtt_bridge: + mqtt_host: localhost + mqtt_port: 1883 + mqtt_username: '' + mqtt_password: '' + mqtt_namespace: hbot + mqtt_ssl: false + mqtt_logger: true + mqtt_notifier: true + mqtt_commands: true + mqtt_events: true + mqtt_external_events: true + mqtt_autostart: true + +# Error log sharing +send_error_logs: true + +# Advanced database options, currently supports SQLAlchemy's included dialects +# Reference: https://docs.sqlalchemy.org/en/13/dialects/ +# To use an instance of SQLite DB the required configuration is +# db_engine: sqlite +# To use a DBMS the required configuration is +# db_host: 127.0.0.1 +# db_port: 3306 +# db_username: username +# db_password: password +# db_name: dbname +db_mode: + db_engine: sqlite + +# Balance Limit Configurations +# e.g. Setting USDT and BTC limits on Binance. +# balance_asset_limit: +# binance: +# BTC: 0.1 +# USDT: 1000 +balance_asset_limit: + ascend_ex: {} + ascend_ex_paper_trade: {} + binance: {} + binance_paper_trade: {} + binance_us: {} + bitfinex: {} + bitmart: {} + bitmex: {} + bitmex_testnet: {} + btc_markets: {} + bybit: {} + bybit_testnet: {} + coinbase_pro: {} + foxbit: {} + gate_io: {} + gate_io_paper_trade: {} + hitbtc: {} + huobi: {} + injective_v2: {} + kraken: {} + kucoin: {} + kucoin_paper_trade: {} + mexc: {} + mock_paper_exchange: {} + ndax: {} + ndax_testnet: {} + okx: {} + polkadex: {} + vertex: {} + vertex_testnet: {} + woo_x: {} + woo_x_testnet: {} + +# Fixed gas price (in Gwei) for Ethereum transactions +manual_gas_price: 50.0 + +# Gateway API Configurations +# default host to only use localhost +# Port need to match the final installation port for Gateway +gateway: + gateway_api_host: localhost + gateway_api_port: '15888' + gateway_use_ssl: true + +# Whether to enable aggregated order and trade data collection +anonymized_metrics_mode: + anonymized_metrics_interval_min: 15.0 + +# A source for rate oracle, currently binance, coin_gecko, coin_cap, dexalot, evedex_perpetual, decibel_perpetual, kucoin, gate_io, backpack, coinbase_advanced_trade, hyperliquid, hyperliquid_perpetual, architect_perpetual, derive, mexc +rate_oracle_source: + name: gate_io + +# A universal token which to display tokens values in, e.g. USD,EUR,BTC +global_token: + global_token_name: USDT + global_token_symbol: $ + usd_equivalent_tokens: + - USD + +# Percentage of API rate limits (on any exchange and any end point) allocated to this bot instance. +# Enter 50 to indicate 50%. E.g. if the API rate limit is 100 calls per second, and you allocate +# 50% to this setting, the bot will have a maximum (limit) of 50 calls per second +rate_limits_share_pct: 100.0 + +commands_timeout: + create_command_timeout: 10.0 + other_commands_timeout: 30.0 + +# Tabulate table format style (https://github.com/astanin/python-tabulate#table-format) +tables_format: psql + +paper_trade: + paper_trade_exchanges: + - binance + - kucoin + - ascend_ex + - gate_io + paper_trade_account_balance: + BTC: 1.0 + DAI: 1000.0 + ETH: 10.0 + ONE: 1000.0 + TUSD: 1000.0 + USDC: 1000.0 + USDQ: 1000.0 + USDT: 1000.0 + WETH: 10.0 + +color: + top_pane: '#000000' + bottom_pane: '#000000' + output_pane: '#262626' + input_pane: '#1C1C1C' + logs_pane: '#121212' + terminal_primary: '#5FFFD7' + primary_label: '#5FFFD7' + secondary_label: '#FFFFFF' + success_label: '#5FFFD7' + warning_label: '#FFFF00' + info_label: '#5FD7FF' + error_label: '#FF0000' + gold_label: '#FFD700' + silver_label: '#C0C0C0' + bronze_label: '#CD7F32' + +# The tick size is the frequency with which the clock notifies the time iterators by calling the +# c_tick() method, that means for example that if the tick size is 1, the logic of the strategy +# will run every second. +tick_size: 1.0 + +market_data_collection: + market_data_collection_enabled: false + market_data_collection_interval: 60 + market_data_collection_depth: 20 diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/conf_fee_overrides.yml b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/conf_fee_overrides.yml new file mode 100644 index 00000000..8dc9d57e --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/conf_fee_overrides.yml @@ -0,0 +1,526 @@ +######################################## +### Fee overrides configurations ### +######################################## + +# For more detailed information: https://docs.hummingbot.io +template_version: 14 + +# Example of the fields that can be specified to override the `TradeFeeFactory` default settings. +# If the field is missing or the value is left blank, the default value will be used. +# The percentage values are specified as 0.1 for 0.1%. +# +# [exchange name]_percent_fee_token: +# [exchange name]_maker_percent_fee: +# [exchange name]_taker_percent_fee: +# [exchange name]_buy_percent_fee_deducted_from_returns: # if False, the buy fee is added to the order costs +# [exchange name]_maker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]]) +# [exchange name]_taker_fixed_fees: # a list of lists of token-fee pairs (e.g. [["ETH", 1]]) + +binance_percent_fee_token: # BNB +binance_maker_percent_fee: # 0.75 +binance_taker_percent_fee: # 0.75 +binance_buy_percent_fee_deducted_from_returns: # True + +# List of supported Exchanges for which the user's conf/conf_fee_override.yml +# will work. This file currently needs to be in sync with hummingbot list of +# supported exchanges +ascend_ex_buy_percent_fee_deducted_from_returns: +ascend_ex_maker_fixed_fees: +ascend_ex_maker_percent_fee: +ascend_ex_percent_fee_token: +ascend_ex_taker_fixed_fees: +ascend_ex_taker_percent_fee: +binance_maker_fixed_fees: +binance_perpetual_buy_percent_fee_deducted_from_returns: +binance_perpetual_maker_fixed_fees: +binance_perpetual_maker_percent_fee: +binance_perpetual_percent_fee_token: +binance_perpetual_taker_fixed_fees: +binance_perpetual_taker_percent_fee: +binance_perpetual_testnet_buy_percent_fee_deducted_from_returns: +binance_perpetual_testnet_maker_fixed_fees: +binance_perpetual_testnet_maker_percent_fee: +binance_perpetual_testnet_percent_fee_token: +binance_perpetual_testnet_taker_fixed_fees: +binance_perpetual_testnet_taker_percent_fee: +binance_taker_fixed_fees: +binance_us_buy_percent_fee_deducted_from_returns: +binance_us_maker_fixed_fees: +binance_us_maker_percent_fee: +binance_us_percent_fee_token: +binance_us_taker_fixed_fees: +binance_us_taker_percent_fee: +bitfinex_buy_percent_fee_deducted_from_returns: +bitfinex_maker_fixed_fees: +bitfinex_maker_percent_fee: +bitfinex_percent_fee_token: +bitfinex_taker_fixed_fees: +bitfinex_taker_percent_fee: +bitmart_buy_percent_fee_deducted_from_returns: +bitmart_maker_fixed_fees: +bitmart_maker_percent_fee: +bitmart_percent_fee_token: +bitmart_taker_fixed_fees: +bitmart_taker_percent_fee: +btc_markets_percent_fee_token: +btc_markets_maker_percent_fee: +btc_markets_taker_percent_fee: +btc_markets_buy_percent_fee_deducted_from_returns: +bybit_perpetual_buy_percent_fee_deducted_from_returns: +bybit_perpetual_maker_fixed_fees: +bybit_perpetual_maker_percent_fee: +bybit_perpetual_percent_fee_token: +bybit_perpetual_taker_fixed_fees: +bybit_perpetual_taker_percent_fee: +bybit_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bybit_perpetual_testnet_maker_fixed_fees: +bybit_perpetual_testnet_maker_percent_fee: +bybit_perpetual_testnet_percent_fee_token: +bybit_perpetual_testnet_taker_fixed_fees: +bybit_perpetual_testnet_taker_percent_fee: +coinbase_pro_buy_percent_fee_deducted_from_returns: +coinbase_pro_maker_fixed_fees: +coinbase_pro_maker_percent_fee: +coinbase_pro_percent_fee_token: +coinbase_pro_taker_fixed_fees: +coinbase_pro_taker_percent_fee: +dydx_perpetual_buy_percent_fee_deducted_from_returns: +dydx_perpetual_maker_fixed_fees: +dydx_perpetual_maker_percent_fee: +dydx_perpetual_percent_fee_token: +dydx_perpetual_taker_fixed_fees: +dydx_perpetual_taker_percent_fee: +gate_io_buy_percent_fee_deducted_from_returns: +gate_io_maker_fixed_fees: +gate_io_maker_percent_fee: +gate_io_percent_fee_token: +gate_io_taker_fixed_fees: +gate_io_taker_percent_fee: +hitbtc_buy_percent_fee_deducted_from_returns: +hitbtc_maker_fixed_fees: +hitbtc_maker_percent_fee: +hitbtc_percent_fee_token: +hitbtc_taker_fixed_fees: +hitbtc_taker_percent_fee: +huobi_buy_percent_fee_deducted_from_returns: +huobi_maker_fixed_fees: +huobi_maker_percent_fee: +huobi_percent_fee_token: +huobi_taker_fixed_fees: +huobi_taker_percent_fee: +kraken_buy_percent_fee_deducted_from_returns: +kraken_maker_fixed_fees: +kraken_maker_percent_fee: +kraken_percent_fee_token: +kraken_taker_fixed_fees: +kraken_taker_percent_fee: +kucoin_buy_percent_fee_deducted_from_returns: +kucoin_maker_fixed_fees: +kucoin_maker_percent_fee: +kucoin_percent_fee_token: +kucoin_taker_fixed_fees: +kucoin_taker_percent_fee: +mexc_buy_percent_fee_deducted_from_returns: +mexc_maker_fixed_fees: +mexc_maker_percent_fee: +mexc_percent_fee_token: +mexc_taker_fixed_fees: +mexc_taker_percent_fee: +ndax_buy_percent_fee_deducted_from_returns: +ndax_maker_fixed_fees: +ndax_maker_percent_fee: +ndax_percent_fee_token: +ndax_taker_fixed_fees: +ndax_taker_percent_fee: +ndax_testnet_buy_percent_fee_deducted_from_returns: +ndax_testnet_maker_fixed_fees: +ndax_testnet_maker_percent_fee: +ndax_testnet_percent_fee_token: +ndax_testnet_taker_fixed_fees: +ndax_testnet_taker_percent_fee: +okx_buy_percent_fee_deducted_from_returns: +okx_maker_fixed_fees: +okx_maker_percent_fee: +okx_percent_fee_token: +okx_taker_fixed_fees: +okx_taker_percent_fee: +phemex_perpetual_percent_fee_token: +phemex_perpetual_maker_percent_fee: +phemex_perpetual_taker_percent_fee: +phemex_perpetual_buy_percent_fee_deducted_from_returns: +phemex_perpetual_maker_fixed_fees: +phemex_perpetual_taker_fixed_fees: +phemex_perpetual_testnet_percent_fee_token: +phemex_perpetual_testnet_maker_percent_fee: +phemex_perpetual_testnet_taker_percent_fee: +phemex_perpetual_testnet_buy_percent_fee_deducted_from_returns: +phemex_perpetual_testnet_maker_fixed_fees: +phemex_perpetual_testnet_taker_fixed_fees: +injective_v2_perpetual_percent_fee_token: +injective_v2_perpetual_maker_percent_fee: +injective_v2_perpetual_taker_percent_fee: +injective_v2_perpetual_buy_percent_fee_deducted_from_returns: +injective_v2_perpetual_maker_fixed_fees: +injective_v2_perpetual_taker_fixed_fees: +bitget_perpetual_percent_fee_token: +bitget_perpetual_maker_percent_fee: +bitget_perpetual_taker_percent_fee: +bitget_perpetual_buy_percent_fee_deducted_from_returns: +bitget_perpetual_maker_fixed_fees: +bitget_perpetual_taker_fixed_fees: +bit_com_perpetual_percent_fee_token: +bit_com_perpetual_maker_percent_fee: +bit_com_perpetual_taker_percent_fee: +bit_com_perpetual_buy_percent_fee_deducted_from_returns: +bit_com_perpetual_maker_fixed_fees: +bit_com_perpetual_taker_fixed_fees: +bit_com_perpetual_testnet_percent_fee_token: +bit_com_perpetual_testnet_maker_percent_fee: +bit_com_perpetual_testnet_taker_percent_fee: +bit_com_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bit_com_perpetual_testnet_maker_fixed_fees: +bit_com_perpetual_testnet_taker_fixed_fees: +gate_io_perpetual_percent_fee_token: +gate_io_perpetual_maker_percent_fee: +gate_io_perpetual_taker_percent_fee: +gate_io_perpetual_buy_percent_fee_deducted_from_returns: +gate_io_perpetual_maker_fixed_fees: +gate_io_perpetual_taker_fixed_fees: +bitmex_perpetual_percent_fee_token: +bitmex_perpetual_maker_percent_fee: +bitmex_perpetual_taker_percent_fee: +bitmex_perpetual_buy_percent_fee_deducted_from_returns: +bitmex_perpetual_maker_fixed_fees: +bitmex_perpetual_taker_fixed_fees: +bitmex_perpetual_testnet_percent_fee_token: +bitmex_perpetual_testnet_maker_percent_fee: +bitmex_perpetual_testnet_taker_percent_fee: +bitmex_perpetual_testnet_buy_percent_fee_deducted_from_returns: +bitmex_perpetual_testnet_maker_fixed_fees: +bitmex_perpetual_testnet_taker_fixed_fees: +kucoin_perpetual_percent_fee_token: +kucoin_perpetual_maker_percent_fee: +kucoin_perpetual_taker_percent_fee: +kucoin_perpetual_buy_percent_fee_deducted_from_returns: +kucoin_perpetual_maker_fixed_fees: +kucoin_perpetual_taker_fixed_fees: +bitmex_percent_fee_token: +bitmex_maker_percent_fee: +bitmex_taker_percent_fee: +bitmex_buy_percent_fee_deducted_from_returns: +bitmex_maker_fixed_fees: +bitmex_taker_fixed_fees: +bitmex_testnet_percent_fee_token: +bitmex_testnet_maker_percent_fee: +bitmex_testnet_taker_percent_fee: +bitmex_testnet_buy_percent_fee_deducted_from_returns: +bitmex_testnet_maker_fixed_fees: +bitmex_testnet_taker_fixed_fees: +btc_markets_maker_fixed_fees: +btc_markets_taker_fixed_fees: +woo_x_percent_fee_token: +woo_x_maker_percent_fee: +woo_x_taker_percent_fee: +woo_x_buy_percent_fee_deducted_from_returns: +woo_x_maker_fixed_fees: +woo_x_taker_fixed_fees: +woo_x_testnet_percent_fee_token: +woo_x_testnet_maker_percent_fee: +woo_x_testnet_taker_percent_fee: +woo_x_testnet_buy_percent_fee_deducted_from_returns: +woo_x_testnet_maker_fixed_fees: +woo_x_testnet_taker_fixed_fees: +polkadex_percent_fee_token: +polkadex_maker_percent_fee: +polkadex_taker_percent_fee: +polkadex_buy_percent_fee_deducted_from_returns: +polkadex_maker_fixed_fees: +polkadex_taker_fixed_fees: +bybit_percent_fee_token: +bybit_maker_percent_fee: +bybit_taker_percent_fee: +bybit_buy_percent_fee_deducted_from_returns: +bybit_maker_fixed_fees: +bybit_taker_fixed_fees: +bybit_testnet_percent_fee_token: +bybit_testnet_maker_percent_fee: +bybit_testnet_taker_percent_fee: +bybit_testnet_buy_percent_fee_deducted_from_returns: +bybit_testnet_maker_fixed_fees: +bybit_testnet_taker_fixed_fees: +injective_v2_percent_fee_token: +injective_v2_maker_percent_fee: +injective_v2_taker_percent_fee: +injective_v2_buy_percent_fee_deducted_from_returns: +injective_v2_maker_fixed_fees: +injective_v2_taker_fixed_fees: +vertex_percent_fee_token: +vertex_maker_percent_fee: +vertex_taker_percent_fee: +vertex_buy_percent_fee_deducted_from_returns: +vertex_maker_fixed_fees: +vertex_taker_fixed_fees: +vertex_testnet_percent_fee_token: +vertex_testnet_maker_percent_fee: +vertex_testnet_taker_percent_fee: +vertex_testnet_buy_percent_fee_deducted_from_returns: +vertex_testnet_maker_fixed_fees: +vertex_testnet_taker_fixed_fees: +mock_paper_exchange_percent_fee_token: +mock_paper_exchange_maker_percent_fee: +mock_paper_exchange_taker_percent_fee: +mock_paper_exchange_buy_percent_fee_deducted_from_returns: +mock_paper_exchange_maker_fixed_fees: +mock_paper_exchange_taker_fixed_fees: +kucoin_perpetual_testnet_percent_fee_token: +kucoin_perpetual_testnet_maker_percent_fee: +kucoin_perpetual_testnet_taker_percent_fee: +kucoin_perpetual_testnet_buy_percent_fee_deducted_from_returns: +kucoin_perpetual_testnet_maker_fixed_fees: +kucoin_perpetual_testnet_taker_fixed_fees: +foxbit_percent_fee_token: +foxbit_maker_percent_fee: +foxbit_taker_percent_fee: +foxbit_buy_percent_fee_deducted_from_returns: +foxbit_maker_fixed_fees: +foxbit_taker_fixed_fees: +hyperliquid_perpetual_percent_fee_token: +hyperliquid_perpetual_maker_percent_fee: +hyperliquid_perpetual_taker_percent_fee: +hyperliquid_perpetual_buy_percent_fee_deducted_from_returns: +hyperliquid_perpetual_maker_fixed_fees: +hyperliquid_perpetual_taker_fixed_fees: +hyperliquid_perpetual_testnet_percent_fee_token: +hyperliquid_perpetual_testnet_maker_percent_fee: +hyperliquid_perpetual_testnet_taker_percent_fee: +hyperliquid_perpetual_testnet_buy_percent_fee_deducted_from_returns: +hyperliquid_perpetual_testnet_maker_fixed_fees: +hyperliquid_perpetual_testnet_taker_fixed_fees: +bitmart_perpetual_percent_fee_token: +bitmart_perpetual_maker_percent_fee: +bitmart_perpetual_taker_percent_fee: +bitmart_perpetual_buy_percent_fee_deducted_from_returns: +bitmart_perpetual_maker_fixed_fees: +bitmart_perpetual_taker_fixed_fees: +architect_perpetual_percent_fee_token: +architect_perpetual_maker_percent_fee: +architect_perpetual_taker_percent_fee: +architect_perpetual_buy_percent_fee_deducted_from_returns: +architect_perpetual_maker_fixed_fees: +architect_perpetual_taker_fixed_fees: +architect_perpetual_sandbox_percent_fee_token: +architect_perpetual_sandbox_maker_percent_fee: +architect_perpetual_sandbox_taker_percent_fee: +architect_perpetual_sandbox_buy_percent_fee_deducted_from_returns: +architect_perpetual_sandbox_maker_fixed_fees: +architect_perpetual_sandbox_taker_fixed_fees: +pacifica_perpetual_percent_fee_token: +pacifica_perpetual_maker_percent_fee: +pacifica_perpetual_taker_percent_fee: +pacifica_perpetual_buy_percent_fee_deducted_from_returns: +pacifica_perpetual_maker_fixed_fees: +pacifica_perpetual_taker_fixed_fees: +pacifica_perpetual_testnet_percent_fee_token: +pacifica_perpetual_testnet_maker_percent_fee: +pacifica_perpetual_testnet_taker_percent_fee: +pacifica_perpetual_testnet_buy_percent_fee_deducted_from_returns: +pacifica_perpetual_testnet_maker_fixed_fees: +pacifica_perpetual_testnet_taker_fixed_fees: +lighter_perpetual_percent_fee_token: +lighter_perpetual_maker_percent_fee: +lighter_perpetual_taker_percent_fee: +lighter_perpetual_buy_percent_fee_deducted_from_returns: +lighter_perpetual_maker_fixed_fees: +lighter_perpetual_taker_fixed_fees: +lighter_perpetual_testnet_percent_fee_token: +lighter_perpetual_testnet_maker_percent_fee: +lighter_perpetual_testnet_taker_percent_fee: +lighter_perpetual_testnet_buy_percent_fee_deducted_from_returns: +lighter_perpetual_testnet_maker_fixed_fees: +lighter_perpetual_testnet_taker_fixed_fees: +derive_perpetual_percent_fee_token: +derive_perpetual_maker_percent_fee: +derive_perpetual_taker_percent_fee: +derive_perpetual_buy_percent_fee_deducted_from_returns: +derive_perpetual_maker_fixed_fees: +derive_perpetual_taker_fixed_fees: +derive_perpetual_testnet_percent_fee_token: +derive_perpetual_testnet_maker_percent_fee: +derive_perpetual_testnet_taker_percent_fee: +derive_perpetual_testnet_buy_percent_fee_deducted_from_returns: +derive_perpetual_testnet_maker_fixed_fees: +derive_perpetual_testnet_taker_fixed_fees: +aevo_perpetual_percent_fee_token: +aevo_perpetual_maker_percent_fee: +aevo_perpetual_taker_percent_fee: +aevo_perpetual_buy_percent_fee_deducted_from_returns: +aevo_perpetual_maker_fixed_fees: +aevo_perpetual_taker_fixed_fees: +aevo_perpetual_testnet_percent_fee_token: +aevo_perpetual_testnet_maker_percent_fee: +aevo_perpetual_testnet_taker_percent_fee: +aevo_perpetual_testnet_buy_percent_fee_deducted_from_returns: +aevo_perpetual_testnet_maker_fixed_fees: +aevo_perpetual_testnet_taker_fixed_fees: +dydx_v4_perpetual_percent_fee_token: +dydx_v4_perpetual_maker_percent_fee: +dydx_v4_perpetual_taker_percent_fee: +dydx_v4_perpetual_buy_percent_fee_deducted_from_returns: +dydx_v4_perpetual_maker_fixed_fees: +dydx_v4_perpetual_taker_fixed_fees: +okx_perpetual_percent_fee_token: +okx_perpetual_maker_percent_fee: +okx_perpetual_taker_percent_fee: +okx_perpetual_buy_percent_fee_deducted_from_returns: +okx_perpetual_maker_fixed_fees: +okx_perpetual_taker_fixed_fees: +grvt_perpetual_percent_fee_token: +grvt_perpetual_maker_percent_fee: +grvt_perpetual_taker_percent_fee: +grvt_perpetual_buy_percent_fee_deducted_from_returns: +grvt_perpetual_maker_fixed_fees: +grvt_perpetual_taker_fixed_fees: +grvt_perpetual_testnet_percent_fee_token: +grvt_perpetual_testnet_maker_percent_fee: +grvt_perpetual_testnet_taker_percent_fee: +grvt_perpetual_testnet_buy_percent_fee_deducted_from_returns: +grvt_perpetual_testnet_maker_fixed_fees: +grvt_perpetual_testnet_taker_fixed_fees: +evedex_perpetual_percent_fee_token: +evedex_perpetual_maker_percent_fee: +evedex_perpetual_taker_percent_fee: +evedex_perpetual_buy_percent_fee_deducted_from_returns: +evedex_perpetual_maker_fixed_fees: +evedex_perpetual_taker_fixed_fees: +backpack_perpetual_percent_fee_token: +backpack_perpetual_maker_percent_fee: +backpack_perpetual_taker_percent_fee: +backpack_perpetual_buy_percent_fee_deducted_from_returns: +backpack_perpetual_maker_fixed_fees: +backpack_perpetual_taker_fixed_fees: +decibel_perpetual_percent_fee_token: +decibel_perpetual_maker_percent_fee: +decibel_perpetual_taker_percent_fee: +decibel_perpetual_buy_percent_fee_deducted_from_returns: +decibel_perpetual_maker_fixed_fees: +decibel_perpetual_taker_fixed_fees: +decibel_perpetual_testnet_percent_fee_token: +decibel_perpetual_testnet_maker_percent_fee: +decibel_perpetual_testnet_taker_percent_fee: +decibel_perpetual_testnet_buy_percent_fee_deducted_from_returns: +decibel_perpetual_testnet_maker_fixed_fees: +decibel_perpetual_testnet_taker_fixed_fees: +derive_percent_fee_token: +derive_maker_percent_fee: +derive_taker_percent_fee: +derive_buy_percent_fee_deducted_from_returns: +derive_maker_fixed_fees: +derive_taker_fixed_fees: +derive_testnet_percent_fee_token: +derive_testnet_maker_percent_fee: +derive_testnet_taker_percent_fee: +derive_testnet_buy_percent_fee_deducted_from_returns: +derive_testnet_maker_fixed_fees: +derive_testnet_taker_fixed_fees: +bitrue_percent_fee_token: +bitrue_maker_percent_fee: +bitrue_taker_percent_fee: +bitrue_buy_percent_fee_deducted_from_returns: +bitrue_maker_fixed_fees: +bitrue_taker_fixed_fees: +lambdaplex_percent_fee_token: +lambdaplex_maker_percent_fee: +lambdaplex_taker_percent_fee: +lambdaplex_buy_percent_fee_deducted_from_returns: +lambdaplex_maker_fixed_fees: +lambdaplex_taker_fixed_fees: +dexalot_percent_fee_token: +dexalot_maker_percent_fee: +dexalot_taker_percent_fee: +dexalot_buy_percent_fee_deducted_from_returns: +dexalot_maker_fixed_fees: +dexalot_taker_fixed_fees: +dexalot_testnet_percent_fee_token: +dexalot_testnet_maker_percent_fee: +dexalot_testnet_taker_percent_fee: +dexalot_testnet_buy_percent_fee_deducted_from_returns: +dexalot_testnet_maker_fixed_fees: +dexalot_testnet_taker_fixed_fees: +bing_x_percent_fee_token: +bing_x_maker_percent_fee: +bing_x_taker_percent_fee: +bing_x_buy_percent_fee_deducted_from_returns: +bing_x_maker_fixed_fees: +bing_x_taker_fixed_fees: +kucoin_hft_percent_fee_token: +kucoin_hft_maker_percent_fee: +kucoin_hft_taker_percent_fee: +kucoin_hft_buy_percent_fee_deducted_from_returns: +kucoin_hft_maker_fixed_fees: +kucoin_hft_taker_fixed_fees: +bitstamp_percent_fee_token: +bitstamp_maker_percent_fee: +bitstamp_taker_percent_fee: +bitstamp_buy_percent_fee_deducted_from_returns: +bitstamp_maker_fixed_fees: +bitstamp_taker_fixed_fees: +gemini_percent_fee_token: +gemini_maker_percent_fee: +gemini_taker_percent_fee: +gemini_buy_percent_fee_deducted_from_returns: +gemini_maker_fixed_fees: +gemini_taker_fixed_fees: +backpack_percent_fee_token: +backpack_maker_percent_fee: +backpack_taker_percent_fee: +backpack_buy_percent_fee_deducted_from_returns: +backpack_maker_fixed_fees: +backpack_taker_fixed_fees: +htx_percent_fee_token: +htx_maker_percent_fee: +htx_taker_percent_fee: +htx_buy_percent_fee_deducted_from_returns: +htx_maker_fixed_fees: +htx_taker_fixed_fees: +lighter_percent_fee_token: +lighter_maker_percent_fee: +lighter_taker_percent_fee: +lighter_buy_percent_fee_deducted_from_returns: +lighter_maker_fixed_fees: +lighter_taker_fixed_fees: +lighter_testnet_percent_fee_token: +lighter_testnet_maker_percent_fee: +lighter_testnet_taker_percent_fee: +lighter_testnet_buy_percent_fee_deducted_from_returns: +lighter_testnet_maker_fixed_fees: +lighter_testnet_taker_fixed_fees: +hyperliquid_percent_fee_token: +hyperliquid_maker_percent_fee: +hyperliquid_taker_percent_fee: +hyperliquid_buy_percent_fee_deducted_from_returns: +hyperliquid_maker_fixed_fees: +hyperliquid_taker_fixed_fees: +hyperliquid_testnet_percent_fee_token: +hyperliquid_testnet_maker_percent_fee: +hyperliquid_testnet_taker_percent_fee: +hyperliquid_testnet_buy_percent_fee_deducted_from_returns: +hyperliquid_testnet_maker_fixed_fees: +hyperliquid_testnet_taker_fixed_fees: +coinbase_advanced_trade_percent_fee_token: +coinbase_advanced_trade_maker_percent_fee: +coinbase_advanced_trade_taker_percent_fee: +coinbase_advanced_trade_buy_percent_fee_deducted_from_returns: +coinbase_advanced_trade_maker_fixed_fees: +coinbase_advanced_trade_taker_fixed_fees: +xrpl_percent_fee_token: +xrpl_maker_percent_fee: +xrpl_taker_percent_fee: +xrpl_buy_percent_fee_deducted_from_returns: +xrpl_maker_fixed_fees: +xrpl_taker_fixed_fees: +bitget_percent_fee_token: +bitget_maker_percent_fee: +bitget_taker_percent_fee: +bitget_buy_percent_fee_deducted_from_returns: +bitget_maker_fixed_fees: +bitget_taker_fixed_fees: diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/connectors/.dockerignore b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/connectors/.dockerignore new file mode 100644 index 00000000..ad30bfec --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/connectors/.dockerignore @@ -0,0 +1 @@ +*.yml \ No newline at end of file diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/connectors/.gitignore b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/connectors/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/connectors/xrpl.yml b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/connectors/xrpl.yml new file mode 100644 index 00000000..c4f7a2a4 --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/connectors/xrpl.yml @@ -0,0 +1,20 @@ +####################### +### xrpl config ### +####################### + +connector: xrpl + +xrpl_secret_key: 7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20223231663134393764323839646334653336626339396166636430303635303534227d2c202263697068657274657874223a20226533343635353939656434393262653036336238363338303164343262373930303435393734353536386633656361386131646135363164313934643737222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20223061613133383663313339383462666261653937316239366361303239313639227d2c20226d6163223a202234613536643862613033646536623163316635326463303165373463653337373066636263396362303739303865333864343163346533356430633035363038227d2c202276657273696f6e223a20332c2022616c696173223a2022227d + +wss_node_urls: +- wss://alien-magical-sky.xrp-mainnet.quiknode.pro/cdeda141d5c67711f061b73b1c4bf70eb163c6a8/ + +custom_markets: + SOLO-XRP: + base: SOLO + quote: XRP + base_issuer: rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz + quote_issuer: '' + trading_pair_symbol: null + +max_request_per_minute: 60 diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/controllers/xrpl_xrp_usdc_mm.yml b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/controllers/xrpl_xrp_usdc_mm.yml new file mode 100644 index 00000000..9347f724 --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/controllers/xrpl_xrp_usdc_mm.yml @@ -0,0 +1,39 @@ +buy_amounts_pct: 1,1 +buy_cooldown_time: 30 +buy_position_effectivization_time: 600 +buy_spreads: 0.0010,0.0020 +connector_name: xrpl +controller_name: pmm_mister +controller_type: generic +executor_refresh_time: 30 +global_pnl_reference: position +global_sl_activation_from: target_base +global_sl_enabled: true +global_stop_loss: 0.05 +global_take_profit: 0.03 +global_tp_activation_from: min_base +global_tp_enabled: true +id: xrpl_xrp_usdc_mm +leverage: 1 +manual_kill_switch: false +max_active_executors_by_level: 3 +max_base_pct: 0.7 +min_base_pct: 0.3 +min_skew: 1 +open_order_type: 3 +portfolio_allocation: 0.05 +position_mode: ONEWAY +position_profit_protection: true +price_distance_tolerance: 0.0005 +refresh_tolerance: 0.0005 +sell_amounts_pct: 1,1 +sell_cooldown_time: 30 +sell_position_effectivization_time: 600 +sell_spreads: 0.0015,0.0030 +take_profit: 0.0012 +take_profit_order_type: 3 +target_base_pct: 0.6 +tick_mode: false +tolerance_scaling: 1.2 +total_amount_quote: 100 +trading_pair: XRP-USDC diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/hummingbot_logs.yml b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/hummingbot_logs.yml new file mode 100755 index 00000000..8e65271c --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/hummingbot_logs.yml @@ -0,0 +1,83 @@ +--- +version: 1 +template_version: 12 + +formatters: + simple: + format: "%(asctime)s - %(process)d - %(name)s - %(levelname)s - %(message)s" + +handlers: + console: + class: hummingbot.logger.cli_handler.CLIHandler + level: DEBUG + formatter: simple + stream: ext://sys.stdout + console_warning: + class: hummingbot.logger.cli_handler.CLIHandler + level: WARNING + formatter: simple + stream: ext://sys.stdout + console_info: + class: hummingbot.logger.cli_handler.CLIHandler + level: INFO + formatter: simple + stream: ext://sys.stdout + file_handler: + class: logging.handlers.TimedRotatingFileHandler + level: DEBUG + formatter: simple + filename: $PROJECT_DIR/logs/logs_$STRATEGY_FILE_PATH.log + encoding: utf8 + when: "D" + interval: 1 + backupCount: 7 + "null": + class: logging.NullHandler + level: DEBUG + +loggers: + hummingbot.core.utils.eth_gas_station_lookup: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.logger.log_server_client: + level: WARNING + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.logger.reporting_proxy_handler: + level: WARNING + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.strategy: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.connector: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.client: + level: NETWORK + propagate: false + handlers: [console, file_handler] + mqtt: true + hummingbot.core.event.event_reporter: + level: EVENT_LOG + propagate: false + handlers: [file_handler] + mqtt: false + conf: + level: NETWORK + handlers: ["null"] + propagate: false + mqtt: false + +root: + level: INFO + handlers: [console, file_handler] + mqtt: true diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/scripts/xrpl-xrp-usdc-mm-20260807-185151.yml b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/scripts/xrpl-xrp-usdc-mm-20260807-185151.yml new file mode 100644 index 00000000..4ec11c4d --- /dev/null +++ b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/conf/scripts/xrpl-xrp-usdc-mm-20260807-185151.yml @@ -0,0 +1,4 @@ +controllers_config: +- xrpl_xrp_usdc_mm.yml +max_global_drawdown_quote: 7.0 +script_file_name: v2_with_controllers.py diff --git a/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/data/xrpl-xrp-usdc-mm-20260807-185151.sqlite b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/data/xrpl-xrp-usdc-mm-20260807-185151.sqlite new file mode 100644 index 00000000..52b9cf05 Binary files /dev/null and b/bots/archived/xrpl-xrp-usdc-mm-20260807-185151/data/xrpl-xrp-usdc-mm-20260807-185151.sqlite differ diff --git a/bots/controllers/generic/lp_rebalancer/lp_rebalancer.py b/bots/controllers/generic/lp_rebalancer/lp_rebalancer.py index fd4c43d6..c8cd6a6b 100644 --- a/bots/controllers/generic/lp_rebalancer/lp_rebalancer.py +++ b/bots/controllers/generic/lp_rebalancer/lp_rebalancer.py @@ -58,7 +58,8 @@ class LPRebalancerConfig(ControllerConfigBase): position_offset_pct: Decimal = Field( default=Decimal("0.01"), json_schema_extra={"is_updatable": True}, - description="Offset from current price. Positive = out-of-range (single-sided). Negative = in-range (needs both tokens, autoswap will convert |offset|%)" + description="Offset from current price. Positive = out-of-range (single-sided). " + "Negative = in-range (needs both tokens, autoswap will convert |offset|%)" ) # Rebalance threshold - used to set LP executor's limit prices @@ -184,6 +185,9 @@ def __init__(self, config: LPRebalancerConfig, *args, **kwargs): # Track the executor we created self._current_executor_id: Optional[str] = None + # Set when a FAILED LP executor still reports a live on-chain position; the + # controller halts new position creation until it is recovered manually + self._orphaned_position_address: Optional[str] = None # Track amounts from last closed position (for autoswap sizing) self._last_closed_base_amount: Optional[Decimal] = None @@ -416,6 +420,16 @@ def determine_executor_actions(self) -> List[ExecutorAction]: actions = [] + # An orphaned on-chain position (FAILED close) must be recovered before any new + # position is opened - creating a fresh executor here would stack live exposure + # on top of the stranded one. + if self._orphaned_position_address: + self.logger().debug( + f"Halted: position {self._orphaned_position_address} from a FAILED executor is " + "still open on-chain and requires manual recovery" + ) + return actions + # Handle order executor tracking and completion (for autoswap) if self._pending_swap_side is not None: if not self._swap_executor_id: @@ -509,10 +523,15 @@ def determine_executor_actions(self) -> List[ExecutorAction]: # Previous executor terminated - capture final amounts and update position_hold terminated_executor = self.get_tracked_executor() if terminated_executor: - # Skip position_hold update if executor failed (no tokens were actually deposited/returned) - if terminated_executor.close_type == CloseType.FAILED: + # Skip position_hold update if the executor failed (nothing deposited or + # returned) or ended as an involuntary hold (close exhausted): in the + # latter case base/quote amounts are pool balances of a still-open + # position, and booking them as returned tokens would corrupt the hold. + if (terminated_executor.close_type == CloseType.FAILED + or terminated_executor.custom_info.get("hold_reason")): self.logger().warning( - f"Executor {terminated_executor.id} FAILED - skipping position_hold update" + f"Executor {terminated_executor.id} ended {terminated_executor.close_type} " + "without returning tokens - skipping position_hold update" ) else: self._last_closed_base_amount = Decimal(str(terminated_executor.custom_info.get("base_amount", 0))) @@ -539,16 +558,32 @@ def determine_executor_actions(self) -> List[ExecutorAction]: f"Position hold total: base={self._position_hold_base}, quote={self._position_hold_quote}" ) - # Check if executor FAILED - retry with same side from executor's config + # Check if the executor went terminal abnormally - FAILED (nothing on-chain) + # or an involuntary POSITION_HOLD (close retries exhausted, hold_reason set) executor_failed = terminated_executor and terminated_executor.close_type == CloseType.FAILED + involuntary_hold = bool(terminated_executor and terminated_executor.custom_info.get("hold_reason")) failed_executor_side = None - if executor_failed: + if executor_failed or involuntary_hold: failed_executor_side = terminated_executor.custom_info.get("side") + # A terminal executor that still reports a position address went down on + # the CLOSE side: its deposit is still on-chain (involuntary hold, or a + # legacy FAILED-with-position from a force-stop). Re-opening would stack + # a second position on top of the stranded one. + orphaned_position = terminated_executor.custom_info.get("position_address") + if orphaned_position: + self._orphaned_position_address = orphaned_position + self._current_executor_id = None + self.logger().error( + f"Executor {terminated_executor.id} ended {terminated_executor.close_type} " + f"with position {orphaned_position} still open on-chain. Halting new " + "position creation until the position is closed or recovered manually." + ) + return actions # Capture closed position bounds for side determination (only for successful closes) closed_lower_price = None closed_upper_price = None - if terminated_executor and not executor_failed: + if terminated_executor and not executor_failed and not involuntary_hold: closed_lower_price = Decimal(str(terminated_executor.custom_info.get("lower_price", 0))) closed_upper_price = Decimal(str(terminated_executor.custom_info.get("upper_price", 0))) @@ -556,10 +591,11 @@ def determine_executor_actions(self) -> List[ExecutorAction]: self._current_executor_id = None # Determine side for new position - if executor_failed and failed_executor_side is not None: - # Retry with same side on failure + if (executor_failed or involuntary_hold) and failed_executor_side is not None: + # Retry with same side after any abnormal terminal (FAILED, or an + # involuntary hold that left no on-chain position to recover) side = failed_executor_side - self.logger().info(f"Retrying with same side={side} after executor failure") + self.logger().info(f"Retrying with same side={side} after abnormal executor end") elif not self._initial_position_created: # Initial position: use configured side side = self.config.side @@ -576,7 +612,10 @@ def determine_executor_actions(self) -> List[ExecutorAction]: else: # Price is within old bounds (shouldn't happen with limit-price auto-close) side = self._determine_side_from_price(self._pool_price) - self.logger().info(f"Price {self._pool_price} in range [{closed_lower_price}, {closed_upper_price}] → side={side} from limits") + self.logger().info( + f"Price {self._pool_price} in range [{closed_lower_price}, {closed_upper_price}] " + f"→ side={side} from limits" + ) else: # Fallback to price limits if not self._pool_price: @@ -784,24 +823,25 @@ def _is_price_within_limits(self, price: Decimal, side: TradeType) -> bool: """ Check if price is within configured limits for the position type. """ + # `is not None`: a limit set to exactly 0 is a real bound, not "unset" if side == TradeType.SELL: - if self.config.sell_price_min and price < self.config.sell_price_min: + if self.config.sell_price_min is not None and price < self.config.sell_price_min: return False - if self.config.sell_price_max and price > self.config.sell_price_max: + if self.config.sell_price_max is not None and price > self.config.sell_price_max: return False elif side == TradeType.BUY: - if self.config.buy_price_min and price < self.config.buy_price_min: + if self.config.buy_price_min is not None and price < self.config.buy_price_min: return False - if self.config.buy_price_max and price > self.config.buy_price_max: + if self.config.buy_price_max is not None and price > self.config.buy_price_max: return False else: # RANGE - if self.config.buy_price_min and price < self.config.buy_price_min: + if self.config.buy_price_min is not None and price < self.config.buy_price_min: return False - if self.config.buy_price_max and price > self.config.buy_price_max: + if self.config.buy_price_max is not None and price > self.config.buy_price_max: return False - if self.config.sell_price_min and price < self.config.sell_price_min: + if self.config.sell_price_min is not None and price < self.config.sell_price_min: return False - if self.config.sell_price_max and price > self.config.sell_price_max: + if self.config.sell_price_max is not None and price > self.config.sell_price_max: return False return True @@ -941,7 +981,8 @@ def to_format_status(self) -> List[str]: width = self.config.position_width_pct offset = self.config.position_offset_pct threshold = self.config.rebalance_threshold_pct - line = f"| Config: side={side_str}, amount={amt} {self._quote_token}, width={width}%, offset={offset}%, threshold={threshold}%" + line = (f"| Config: side={side_str}, amount={amt} {self._quote_token}, " + f"width={width}%, offset={offset}%, threshold={threshold}%") status.append(line + " " * (box_width - len(line) + 1) + "|") status.append("|" + " " * box_width + "|") @@ -985,7 +1026,8 @@ def to_format_status(self) -> List[str]: lower_limit = Decimal(str(lower_price)) * (Decimal("1") - threshold_pct) upper_limit = Decimal(str(upper_price)) * (Decimal("1") + threshold_pct) - line = f"| Price: {float(self._pool_price):.{price_decimals}f} | Auto-close if: <{float(lower_limit):.{price_decimals}f} or >{float(upper_limit):.{price_decimals}f}" + line = (f"| Price: {float(self._pool_price):.{price_decimals}f} | Auto-close if: " + f"<{float(lower_limit):.{price_decimals}f} or >{float(upper_limit):.{price_decimals}f}") status.append(line + " " * (box_width - len(line) + 1) + "|") state = custom.get("state", "UNKNOWN") @@ -1070,7 +1112,8 @@ def to_format_status(self) -> List[str]: line = f"| Swaps Executed: {len(closed_swaps)}" status.append(line + " " * (box_width - len(line) + 1) + "|") - line = f"| Fees Collected: {float(total_fees_base):.6f} {self._base_token} + {float(total_fees_quote):.6f} {self._quote_token} = {float(total_fees_value):.6f} {self._quote_token}" + line = (f"| Fees Collected: {float(total_fees_base):.6f} {self._base_token} + " + f"{float(total_fees_quote):.6f} {self._quote_token} = {float(total_fees_value):.6f} {self._quote_token}") status.append(line + " " * (box_width - len(line) + 1) + "|") status.append("+" + "-" * box_width + "+") diff --git a/database/models.py b/database/models.py index 6b85af06..84698835 100644 --- a/database/models.py +++ b/database/models.py @@ -325,7 +325,8 @@ class GatewayCLMMEvent(Base): # Event type event_type = Column(String, nullable=False, - index=True) # OPEN, ADD_LIQUIDITY, REMOVE_LIQUIDITY, COLLECT_FEES, CLOSE + index=True) # OPEN, ADD_LIQUIDITY, REMOVE_LIQUIDITY, COLLECT_FEES, CLOSE, + # DISCOVERED (written by the poller with a synthetic tx hash) # Event amounts base_token_amount = Column(Numeric(precision=30, scale=18), nullable=True) diff --git a/database/repositories/executor_repository.py b/database/repositories/executor_repository.py index c6c6c0b4..5ebdd4ae 100644 --- a/database/repositories/executor_repository.py +++ b/database/repositories/executor_repository.py @@ -134,6 +134,28 @@ async def get_executors( result = await self.session.execute(stmt) return list(result.scalars().all()) + async def get_executors_by_close_types( + self, + close_types: List[str], + executor_type: Optional[str] = None, + limit: Optional[int] = 500, + ) -> List[ExecutorRecord]: + """Get executors whose close_type is one of the given values, newest first. + + Filter by executor_type in SQL where possible: applying `limit` to a broad + candidate set and filtering in Python can silently drop the very rows the + caller is looking for once the newest N candidates are all irrelevant. + """ + stmt = select(ExecutorRecord).where(ExecutorRecord.close_type.in_(close_types)) + if executor_type: + stmt = stmt.where(ExecutorRecord.executor_type == executor_type) + stmt = stmt.order_by(desc(ExecutorRecord.created_at)) + if limit is not None: + stmt = stmt.limit(limit) + + result = await self.session.execute(stmt) + return list(result.scalars().all()) + async def get_active_executors( self, account_name: Optional[str] = None, diff --git a/database/repositories/gateway_clmm_repository.py b/database/repositories/gateway_clmm_repository.py index f292c69f..c5281767 100644 --- a/database/repositories/gateway_clmm_repository.py +++ b/database/repositories/gateway_clmm_repository.py @@ -1,8 +1,8 @@ -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from decimal import Decimal from typing import Dict, List, Optional, Set -from sqlalchemy import distinct, select +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from database.models import GatewayCLMMEvent, GatewayCLMMPosition @@ -93,7 +93,7 @@ async def close_position(self, position_address: str) -> Optional[GatewayCLMMPos position = result.scalar_one_or_none() if position: position.status = "CLOSED" - position.closed_at = datetime.utcnow() + position.closed_at = datetime.now(timezone.utc) await self.session.flush() return position @@ -114,6 +114,44 @@ async def reopen_position(self, position_address: str) -> Optional[GatewayCLMMPo await self.session.flush() return position + async def add_to_initial_amounts( + self, + position_address: str, + base_delta: Decimal, + quote_delta: Decimal + ) -> Optional[GatewayCLMMPosition]: + """Raise the PnL baseline when liquidity is ADDED to an existing position. + + Without this, pnl_summary compares post-add value against the original + deposit only and overstates PnL by exactly the added capital. + """ + result = await self.session.execute( + select(GatewayCLMMPosition).where(GatewayCLMMPosition.position_address == position_address) + ) + position = result.scalar_one_or_none() + if position: + position.initial_base_token_amount = float(position.initial_base_token_amount or 0) + float(base_delta) + position.initial_quote_token_amount = float(position.initial_quote_token_amount or 0) + float(quote_delta) + await self.session.flush() + return position + + async def get_recently_closed_addresses(self, within_seconds: int) -> Set[str]: + """Addresses of positions closed within the last `within_seconds`. + + Used by the discovery sweep's reopen logic: a lagging positions-owned RPC + read can still show a position the user just closed, and reopening it would + flap the record CLOSED -> OPEN -> CLOSED. + """ + cutoff = datetime.now(timezone.utc) - timedelta(seconds=within_seconds) + result = await self.session.execute( + select(GatewayCLMMPosition.position_address).where( + GatewayCLMMPosition.status == "CLOSED", + GatewayCLMMPosition.closed_at.isnot(None), + GatewayCLMMPosition.closed_at >= cutoff, + ) + ) + return set(result.scalars().all()) + async def get_positions( self, network: Optional[str] = None, @@ -162,32 +200,6 @@ async def get_open_positions( limit=1000 ) - async def get_unique_wallet_configs(self) -> List[Dict]: - """ - Get unique combinations of connector/network/wallet from all positions. - - Returns: - List of dicts with keys: connector, network, wallet_address - This is useful for discovering which wallets to poll for positions. - """ - query = select( - distinct(GatewayCLMMPosition.connector), - GatewayCLMMPosition.network, - GatewayCLMMPosition.wallet_address - ).distinct() - - result = await self.session.execute(query) - rows = result.all() - - return [ - { - "connector": row[0], - "network": row[1], - "wallet_address": row[2] - } - for row in rows - ] - async def get_position_addresses_set(self, status: Optional[str] = None) -> Set[str]: """ Get a set of position addresses in the database. @@ -216,19 +228,6 @@ async def create_event(self, event_data: Dict) -> GatewayCLMMEvent: await self.session.flush() return event - async def get_event_by_tx_hash( - self, - transaction_hash: str, - event_type: Optional[str] = None - ) -> Optional[GatewayCLMMEvent]: - """Get an event by transaction hash.""" - query = select(GatewayCLMMEvent).where(GatewayCLMMEvent.transaction_hash == transaction_hash) - if event_type: - query = query.where(GatewayCLMMEvent.event_type == event_type) - - result = await self.session.execute(query) - return result.scalar_one_or_none() - async def update_event_status( self, transaction_hash: str, @@ -294,14 +293,14 @@ def position_to_dict(self, position: GatewayCLMMPosition) -> Dict: pnl_summary = None # Get prices for PnL calculation - entry_price = float(position.entry_price) if position.entry_price else None - current_price = float(position.current_price) if position.current_price else None + entry_price = float(position.entry_price) if position.entry_price is not None else None + current_price = float(position.current_price) if position.current_price is not None else None # Calculate PnL if we have initial amounts and prices - if (position.initial_base_token_amount is not None and - position.initial_quote_token_amount is not None and - entry_price and entry_price > 0 and - current_price and current_price > 0): + if (position.initial_base_token_amount is not None + and position.initial_quote_token_amount is not None + and entry_price and entry_price > 0 + and current_price and current_price > 0): # Initial amounts initial_base = float(position.initial_base_token_amount) @@ -411,8 +410,10 @@ def position_to_dict(self, position: GatewayCLMMPosition) -> Dict: "entry_price": entry_price, "current_price": current_price, "percentage": float(position.percentage) if position.percentage is not None else None, - "initial_base_token_amount": float(position.initial_base_token_amount) if position.initial_base_token_amount is not None else None, - "initial_quote_token_amount": float(position.initial_quote_token_amount) if position.initial_quote_token_amount is not None else None, + "initial_base_token_amount": (float(position.initial_base_token_amount) + if position.initial_base_token_amount is not None else None), + "initial_quote_token_amount": (float(position.initial_quote_token_amount) + if position.initial_quote_token_amount is not None else None), "position_rent": float(position.position_rent) if position.position_rent is not None else None, "base_token_amount": float(position.base_token_amount), "quote_token_amount": float(position.quote_token_amount), @@ -431,11 +432,11 @@ def event_to_dict(self, event: GatewayCLMMEvent) -> Dict: "transaction_hash": event.transaction_hash, "timestamp": event.timestamp.isoformat(), "event_type": event.event_type, - "base_token_amount": float(event.base_token_amount) if event.base_token_amount else None, - "quote_token_amount": float(event.quote_token_amount) if event.quote_token_amount else None, - "base_fee_collected": float(event.base_fee_collected) if event.base_fee_collected else None, - "quote_fee_collected": float(event.quote_fee_collected) if event.quote_fee_collected else None, - "gas_fee": float(event.gas_fee) if event.gas_fee else None, + "base_token_amount": float(event.base_token_amount) if event.base_token_amount is not None else None, + "quote_token_amount": float(event.quote_token_amount) if event.quote_token_amount is not None else None, + "base_fee_collected": float(event.base_fee_collected) if event.base_fee_collected is not None else None, + "quote_fee_collected": float(event.quote_fee_collected) if event.quote_fee_collected is not None else None, + "gas_fee": float(event.gas_fee) if event.gas_fee is not None else None, "gas_token": event.gas_token, "status": event.status, "error_message": event.error_message, diff --git a/database/repositories/gateway_swap_repository.py b/database/repositories/gateway_swap_repository.py index c5aea52f..1dd29625 100644 --- a/database/repositories/gateway_swap_repository.py +++ b/database/repositories/gateway_swap_repository.py @@ -1,4 +1,5 @@ -from datetime import datetime +import logging +from datetime import datetime, timezone from decimal import Decimal from typing import Dict, List, Optional @@ -7,6 +8,8 @@ from database.models import GatewaySwap +logger = logging.getLogger(__name__) + class GatewaySwapRepository: def __init__(self, session: AsyncSession): @@ -69,18 +72,23 @@ async def get_swaps( if network: query = query.where(GatewaySwap.network == network) if connector: - query = query.where(GatewaySwap.connector == connector) + # Rows store the base venue name; accept a typed provider + # ("jupiter/router") as the same filter. + query = query.where(GatewaySwap.connector == connector.split("/")[0]) if wallet_address: query = query.where(GatewaySwap.wallet_address == wallet_address) if trading_pair: query = query.where(GatewaySwap.trading_pair == trading_pair) if status: - query = query.where(GatewaySwap.status == status) + # DB statuses are uppercase (SUBMITTED/CONFIRMED/FAILED); accept either + # casing so a status copied from a write response matches. + query = query.where(GatewaySwap.status == status.upper()) if start_time: - start_dt = datetime.fromtimestamp(start_time) + # tz-aware: the timestamp column is TIMESTAMP(timezone=True) + start_dt = datetime.fromtimestamp(start_time, tz=timezone.utc) query = query.where(GatewaySwap.timestamp >= start_dt) if end_time: - end_dt = datetime.fromtimestamp(end_time) + end_dt = datetime.fromtimestamp(end_time, tz=timezone.utc) query = query.where(GatewaySwap.timestamp <= end_dt) # Apply ordering and pagination @@ -112,19 +120,29 @@ async def get_swaps_summary( wallet_address=wallet_address, start_time=start_time, end_time=end_time, - limit=10000 # Get all for summary + limit=10000 ) + if len(swaps) == 10000: + logger.warning("Swap summary hit the 10,000-row cap; totals cover only the most recent rows") total_swaps = len(swaps) confirmed_swaps = sum(1 for s in swaps if s.status == "CONFIRMED") failed_swaps = sum(1 for s in swaps if s.status == "FAILED") pending_swaps = sum(1 for s in swaps if s.status == "SUBMITTED") - # Calculate total volume (in quote token) - total_volume = sum( - float(s.output_amount if s.side == "BUY" else s.input_amount) - for s in swaps if s.status == "CONFIRMED" - ) + # Quote-denominated volume per quote token. Amount columns are token-flow + # (BUY: input=quote, output=base; SELL: input=base, output=quote), so the + # quote leg is input for BUY and output for SELL — and volumes are only + # comparable within one quote token, never summed across denominations. + # side.upper() tolerates legacy lowercase rows written before normalization. + volume_by_quote_token: Dict[str, float] = {} + for s in swaps: + if s.status != "CONFIRMED": + continue + quote_amount = s.input_amount if (s.side or "").upper() == "BUY" else s.output_amount + volume_by_quote_token[s.quote_token] = ( + volume_by_quote_token.get(s.quote_token, 0.0) + float(quote_amount) + ) # Calculate total gas fees total_gas_fees = sum( @@ -138,7 +156,7 @@ async def get_swaps_summary( "failed_swaps": failed_swaps, "pending_swaps": pending_swaps, "success_rate": confirmed_swaps / total_swaps if total_swaps > 0 else 0, - "total_volume": total_volume, + "volume_by_quote_token": volume_by_quote_token, "total_gas_fees": total_gas_fees, } @@ -153,12 +171,14 @@ def to_dict(self, swap: GatewaySwap) -> Dict: "trading_pair": swap.trading_pair, "base_token": swap.base_token, "quote_token": swap.quote_token, - "side": swap.side, + # Legacy rows written before normalization may hold lowercase; serve uppercase + "side": (swap.side or "").upper(), "input_amount": float(swap.input_amount), "output_amount": float(swap.output_amount), "price": float(swap.price), - "slippage_pct": float(swap.slippage_pct) if swap.slippage_pct else None, - "gas_fee": float(swap.gas_fee) if swap.gas_fee else None, + # `is not None`: a recorded slippage of 0 is a real value, not "unset" + "slippage_pct": float(swap.slippage_pct) if swap.slippage_pct is not None else None, + "gas_fee": float(swap.gas_fee) if swap.gas_fee is not None else None, "gas_token": swap.gas_token, "status": swap.status, "pool_address": swap.pool_address, diff --git a/models/__init__.py b/models/__init__.py index 8ecd9eb3..e7ef8444 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -70,6 +70,8 @@ ExecutorFilterRequest, ExecutorResponse, ExecutorsSummaryResponse, + OrphanedPositionRecord, + OrphanedPositionsResponse, StopExecutorRequest, StopExecutorResponse, ) @@ -92,39 +94,35 @@ AMMAddLiquidityRequest, AMMCreatePoolRequest, AMMCreatePoolResponse, - AMMExecuteSwapRequest, AMMPoolInfoResponse, AMMPositionDetail, AMMPositionInfoResponse, AMMPositionsOwnedRequest, AMMQuoteLiquidityRequest, AMMQuoteLiquidityResponse, - AMMQuoteSwapRequest, - AMMQuoteSwapResponse, AMMRemoveLiquidityRequest, AMMTransactionResponse, CLMMAddLiquidityRequest, CLMMClosePositionRequest, + CLMMClosePositionResponse, CLMMCollectFeesRequest, CLMMCollectFeesResponse, - CLMMGetPositionInfoRequest, + CLMMCreatePoolRequest, CLMMOpenPositionRequest, CLMMOpenPositionResponse, CLMMPoolBin, - CLMMPoolInfoRequest, CLMMPoolInfoResponse, CLMMPoolListItem, CLMMPoolListResponse, CLMMPositionInfo, CLMMPositionsOwnedRequest, + CLMMQuotePositionRequest, + CLMMQuotePositionResponse, CLMMRemoveLiquidityRequest, - GetPoolInfoRequest, - PoolInfo, SwapExecuteRequest, SwapExecuteResponse, SwapQuoteRequest, SwapQuoteResponse, - TimeBasedMetrics, ) # Market data models @@ -320,9 +318,6 @@ "AMMPoolInfoResponse", "AMMPositionDetail", "AMMPositionInfoResponse", - "AMMQuoteSwapRequest", - "AMMQuoteSwapResponse", - "AMMExecuteSwapRequest", "AMMTransactionResponse", "AMMQuoteLiquidityRequest", "AMMQuoteLiquidityResponse", @@ -339,16 +334,15 @@ "CLMMRemoveLiquidityRequest", "CLMMClosePositionRequest", "CLMMCollectFeesRequest", + "CLMMClosePositionResponse", + "CLMMCreatePoolRequest", "CLMMCollectFeesResponse", "CLMMPositionsOwnedRequest", + "CLMMQuotePositionRequest", + "CLMMQuotePositionResponse", "CLMMPositionInfo", - "CLMMGetPositionInfoRequest", - "CLMMPoolInfoRequest", "CLMMPoolBin", "CLMMPoolInfoResponse", - "GetPoolInfoRequest", - "PoolInfo", - "TimeBasedMetrics", "CLMMPoolListItem", "CLMMPoolListResponse", # Portfolio models @@ -384,4 +378,6 @@ "ExecutorResponse", "ExecutorDetailResponse", "ExecutorsSummaryResponse", + "OrphanedPositionRecord", + "OrphanedPositionsResponse", ] diff --git a/models/executors.py b/models/executors.py index 278fd2a2..c1e5f774 100644 --- a/models/executors.py +++ b/models/executors.py @@ -421,8 +421,66 @@ class CreateExecutorResponse(BaseModel): class StopExecutorResponse(BaseModel): """Response after stopping an executor.""" executor_id: str = Field(description="Executor identifier") - status: str = Field(description="New status (usually 'stopping')") + status: str = Field(description="New status: 'stopping', or 'already_terminated' when the stop was a no-op") keep_position: bool = Field(description="Whether position was kept open") + close_type: Optional[str] = Field(default=None, description="Final close_type when already terminated") + position_address: Optional[str] = Field( + default=None, description="On-chain position address from the executor's final state, if any" + ) + orphaned_position: bool = Field( + default=False, + description="True when the executor terminated with a live on-chain position that needs recovery" + ) + hold_reason: Optional[str] = Field( + default=None, + description="Why a POSITION_HOLD terminal was involuntary (e.g. close_retries_exhausted); None for voluntary holds" + ) + + +class OrphanedPositionRecord(BaseModel): + """A terminated executor that may still own an on-chain position.""" + executor_id: str = Field(description="Executor identifier") + executor_type: str = Field(description="Executor type (e.g. lp_executor)") + account_name: Optional[str] = Field(default=None, description="Account name") + connector_name: Optional[str] = Field( + default=None, + description="Connector name. For lp_executor this is the network id (e.g. 'solana-mainnet-beta'), " + "not the DEX - see lp_provider for the DEX" + ) + trading_pair: Optional[str] = Field(default=None, description="Trading pair") + lp_provider: Optional[str] = Field( + default=None, + description="DEX connector that holds the position (e.g. 'orca/clmm'), read from the executor config. " + "Pass its base name to the CLMM close endpoint" + ) + pool_address: Optional[str] = Field( + default=None, + description="Pool the position was opened against, read from the executor config. Required to close a " + "position that was never recorded in the API database (LP-executor positions never are)" + ) + controller_id: str = Field(default="main", description="Controller/agent grouping label") + close_type: Optional[str] = Field( + default=None, description="POSITION_HOLD (involuntary hold), FAILED, or SYSTEM_CLEANUP" + ) + closed_at: Optional[str] = Field(default=None, description="Termination timestamp (ISO format)") + position_address: Optional[str] = Field( + default=None, description="On-chain position address (None for restart cleanups, which never persisted state)" + ) + state: Optional[str] = Field(default=None, description="Executor state at termination (e.g. FAILED, CLOSING)") + hold_reason: Optional[str] = Field( + default=None, + description="Why the hold was involuntary (e.g. close_retries_exhausted); None for legacy FAILED/SYSTEM_CLEANUP records" + ) + needs_onchain_reconciliation: bool = Field( + default=False, + description="True when the position address is unknown and on-chain state must be checked externally" + ) + + +class OrphanedPositionsResponse(BaseModel): + """Terminated executors that may have stranded on-chain positions.""" + count: int = Field(description="Number of orphan candidates") + orphans: List[OrphanedPositionRecord] = Field(description="Orphan candidate records") class ExecutorsSummaryResponse(BaseModel): diff --git a/models/gateway.py b/models/gateway.py index 87d6d862..9ab63269 100644 --- a/models/gateway.py +++ b/models/gateway.py @@ -19,7 +19,7 @@ class GatewayConfig(BaseModel): clients (which use ``CONFIG_PASSWORD``). The passphrase is therefore always ``CONFIG_PASSWORD``; a separate value would only break the API<->Gateway mTLS chain. """ - image: str = Field(default="hummingbot/gateway:latest", description="Docker image for Gateway") + image: str = Field(default="hummingbot/gateway:development", description="Docker image for Gateway") port: int = Field(default=15888, description="Port for Gateway API") diff --git a/models/gateway_trading.py b/models/gateway_trading.py index c9cb9c48..93a8ef66 100644 --- a/models/gateway_trading.py +++ b/models/gateway_trading.py @@ -1,8 +1,8 @@ """ -Models for Gateway DEX trading operations. -Supports swaps via routers (Jupiter, 0x) and CLMM liquidity positions (Meteora, Raydium, Uniswap V3). - -Note: AMM support has been removed. Use Router for simple swaps, CLMM for liquidity provision. +Models for Gateway DEX trading operations, mirroring Gateway's unified /trading routes: +swaps (routers like Jupiter and pool-scoped AMM swaps), CLMM liquidity positions +(Meteora, Raydium, Orca, Uniswap V3, PancakeSwap), and AMM liquidity/pool creation +(Meteora DAMM v2, Raydium CPMM, Uniswap V2). """ from decimal import Decimal from typing import Any, Dict, List, Optional @@ -13,27 +13,52 @@ # Swap Models (Router: Jupiter, 0x) # ============================================ + class SwapQuoteRequest(BaseModel): """Request for swap price quote""" connector: str = Field(description="DEX router connector (e.g., 'jupiter', '0x')") network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta', 'ethereum-mainnet')") trading_pair: str = Field(description="Trading pair in BASE-QUOTE format (e.g., 'SOL-USDC')") side: str = Field(description="Trade side: 'BUY' or 'SELL'") - amount: Decimal = Field(description="Amount to swap (in base token for SELL, quote token for BUY)") - slippage_pct: Optional[Decimal] = Field(default=1.0, description="Maximum slippage percentage (default: 1.0)") + amount: Decimal = Field( + description="Amount denominated in the BASE token (SELL: base to sell; BUY: base to receive — " + "Gateway quotes BUY as ExactOut)") + slippage_pct: Optional[Decimal] = Field( + default=None, description="Maximum slippage percentage; omit to use the connector's configured slippagePct") + extra_params: Optional[Dict[str, Any]] = Field( + default=None, + description="Connector-specific params passed through to Gateway under its own names: " + "approximateIfNoExactOut (Solana routers). Unknown keys are rejected.") class SwapQuoteResponse(BaseModel): - """Response with swap quote details""" + """Swap quote, re-framed from Gateway's token-flow response into trading-pair terms. + + Gateway's /trading/swap/quote speaks tokenIn/tokenOut; this keeps the base/quote + + side framing bots use and passes Gateway's execution-safety fields through in + snake_case. No gas estimate: Gateway's quote does not return one. + """ base: str = Field(description="Base token symbol") quote: str = Field(description="Quote token symbol") price: Decimal = Field(description="Quoted price (base/quote)") amount: Decimal = Field(description="Amount specified in request (BUY: base amount to receive, SELL: base amount to sell)") - amount_in: Optional[Decimal] = Field(default=None, description="Actual input amount (BUY: quote to spend, SELL: base to sell)") - amount_out: Optional[Decimal] = Field(default=None, description="Actual output amount (BUY: base to receive, SELL: quote to receive)") - expected_amount: Optional[Decimal] = Field(default=None, description="Deprecated: use amount_out instead") - slippage_pct: Decimal = Field(description="Applied slippage percentage") - gas_estimate: Optional[Decimal] = Field(default=None, description="Estimated gas cost") + amount_in: Optional[Decimal] = Field( + default=None, description="Actual input amount (BUY: quote to spend, SELL: base to sell)" + ) + amount_out: Optional[Decimal] = Field( + default=None, description="Actual output amount (BUY: base to receive, SELL: quote to receive)" + ) + min_amount_out: Optional[Decimal] = Field( + default=None, description="Minimum output the transaction will accept after slippage") + max_amount_in: Optional[Decimal] = Field( + default=None, description="Maximum input the transaction will spend after slippage") + price_impact_pct: Optional[Decimal] = Field( + default=None, description="Price impact of this trade size on the route") + pool_address: Optional[str] = Field(default=None, description="Pool the quote was priced against") + route_path: Optional[str] = Field(default=None, description="Route taken (router connectors)") + slippage_pct: Optional[Decimal] = Field( + default=None, + description="Slippage percentage Gateway applied to the quote (the request value when Gateway omits it)") class SwapExecuteRequest(BaseModel): @@ -42,9 +67,15 @@ class SwapExecuteRequest(BaseModel): network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta')") trading_pair: str = Field(description="Trading pair (e.g., 'SOL-USDC')") side: str = Field(description="Trade side: 'BUY' or 'SELL'") - amount: Decimal = Field(description="Amount to swap") - slippage_pct: Optional[Decimal] = Field(default=1.0, description="Maximum slippage percentage (default: 1.0)") + amount: Decimal = Field( + description="Amount denominated in the BASE token (SELL: base to sell; BUY: base to receive)") + slippage_pct: Optional[Decimal] = Field( + default=None, description="Maximum slippage percentage; omit to use the connector's configured slippagePct") wallet_address: Optional[str] = Field(default=None, description="Wallet address (optional, uses default if not provided)") + extra_params: Optional[Dict[str, Any]] = Field( + default=None, + description="Connector-specific params passed through to Gateway under its own names: " + "approximateIfNoExactOut (Solana routers). Unknown keys are rejected.") class SwapExecuteResponse(BaseModel): @@ -73,7 +104,8 @@ class CLMMOpenPositionRequest(BaseModel): # Initial liquidity base_token_amount: Optional[Decimal] = Field(default=None, description="Amount of base token to add") quote_token_amount: Optional[Decimal] = Field(default=None, description="Amount of quote token to add") - slippage_pct: Optional[Decimal] = Field(default=1.0, description="Maximum slippage percentage (default: 1.0)") + slippage_pct: Optional[Decimal] = Field( + default=None, description="Maximum slippage percentage; omit to use the connector's configured slippagePct") wallet_address: Optional[str] = Field(default=None, description="Wallet address (optional, uses default if not provided)") # Connector-specific parameters (e.g., strategyType for Meteora) @@ -83,11 +115,23 @@ class CLMMOpenPositionRequest(BaseModel): class CLMMOpenPositionResponse(BaseModel): """Response after opening a new CLMM position""" transaction_hash: str = Field(description="Transaction hash") - position_address: str = Field(description="Address of the newly created position") + position_address: Optional[str] = Field( + default=None, + description="Address of the newly created position. None when the transaction was " + "submitted but not yet confirmed (Gateway only knows the address once the tx lands) — " + "poll the transaction; the poller records the position once it appears on-chain") trading_pair: str = Field(description="Trading pair") pool_address: str = Field(description="Pool address") lower_price: Decimal = Field(description="Lower price bound") upper_price: Decimal = Field(description="Upper price bound") + base_token_amount_added: Optional[Decimal] = Field( + default=None, + description="Base amount actually added on-chain (confirmed txs only; the requested amount otherwise)") + quote_token_amount_added: Optional[Decimal] = Field( + default=None, + description="Quote amount actually added on-chain (confirmed txs only; the requested amount otherwise)") + position_rent: Optional[Decimal] = Field( + default=None, description="Native token locked as rent for the position account (refunded on close)") status: str = Field(default="submitted", description="Transaction status") @@ -98,16 +142,26 @@ class CLMMAddLiquidityRequest(BaseModel): position_address: str = Field(description="Existing position address to add liquidity to") base_token_amount: Optional[Decimal] = Field(default=None, description="Amount of base token to add") quote_token_amount: Optional[Decimal] = Field(default=None, description="Amount of quote token to add") - slippage_pct: Optional[Decimal] = Field(default=1.0, description="Maximum slippage percentage (default: 1.0)") + slippage_pct: Optional[Decimal] = Field( + default=None, description="Maximum slippage percentage; omit to use the connector's configured slippagePct") wallet_address: Optional[str] = Field(default=None, description="Wallet address (optional, uses default if not provided)") + # Connector-specific parameters (e.g., strategyType for Meteora) + extra_params: Optional[Dict[str, Any]] = Field(default=None, description="Additional connector-specific parameters") + class CLMMRemoveLiquidityRequest(BaseModel): """Request to remove SOME liquidity from a CLMM position (partial removal)""" connector: str = Field(description="CLMM connector (e.g., 'meteora', 'raydium', 'uniswap')") network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta')") position_address: str = Field(description="Position address to remove liquidity from") - percentage: Decimal = Field(description="Percentage of liquidity to remove (0-100)") + # Same name as the AMM remove model and Gateway's percentageToRemove — and distinct + # from the position row's `percentage`, which means price-range width. + percentage_to_remove: Decimal = Field(description="Percentage of liquidity to remove (0-100)") + slippage_pct: Optional[Decimal] = Field( + default=None, + description="Maximum slippage percentage. Only honored by the Orca connector; " + "omit to use the connector's configured slippagePct") wallet_address: Optional[str] = Field(default=None, description="Wallet address (optional, uses default if not provided)") @@ -116,6 +170,11 @@ class CLMMClosePositionRequest(BaseModel): connector: str = Field(description="CLMM connector (e.g., 'meteora', 'raydium', 'uniswap')") network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta')") position_address: str = Field(description="Position address to close") + pool_address: Optional[str] = Field( + default=None, + description="Pool the position belongs to. Informational only — neither Gateway's call " + "nor the fee snapshot needs it, and unrecorded positions work without it" + ) wallet_address: Optional[str] = Field(default=None, description="Wallet address (optional, uses default if not provided)") @@ -124,6 +183,11 @@ class CLMMCollectFeesRequest(BaseModel): connector: str = Field(description="CLMM connector (e.g., 'meteora', 'raydium', 'uniswap')") network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta')") position_address: str = Field(description="Position address to collect fees from") + pool_address: Optional[str] = Field( + default=None, + description="Pool the position belongs to. Informational only — neither Gateway's call " + "nor the fee snapshot needs it, and unrecorded positions work without it" + ) wallet_address: Optional[str] = Field(default=None, description="Wallet address (optional, uses default if not provided)") @@ -136,21 +200,93 @@ class CLMMCollectFeesResponse(BaseModel): status: str = Field(default="submitted", description="Transaction status") +class CLMMClosePositionResponse(CLMMCollectFeesResponse): + """Response after closing a position: fees collected plus what the close returned. + + The removed amounts and rent refund come from Gateway's confirmed transaction data, + so they are None for submitted-not-confirmed transactions. + """ + base_token_amount_removed: Optional[Decimal] = Field( + default=None, description="Base liquidity actually withdrawn on-chain") + quote_token_amount_removed: Optional[Decimal] = Field( + default=None, description="Quote liquidity actually withdrawn on-chain") + position_rent_refunded: Optional[Decimal] = Field( + default=None, description="Native token rent refunded when the position account closed") + + +class CLMMQuotePositionRequest(BaseModel): + """Request to quote a candidate CLMM position before opening or adding. + + Mirrors Gateway's GET /trading/clmm/quote-position: given the price range and + one or both deposit amounts, returns the actual base/quote split the pool + would take (and which side limits it) without signing anything. + """ + connector: str = Field(description="CLMM connector (e.g., 'meteora', 'raydium', 'orca')") + network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta')") + pool_address: str = Field(description="Pool contract address") + lower_price: Decimal = Field(description="Lower price bound") + upper_price: Decimal = Field(description="Upper price bound") + base_token_amount: Optional[Decimal] = Field(default=None, description="Base amount to deposit (one side may be omitted)") + quote_token_amount: Optional[Decimal] = Field(default=None, description="Quote amount to deposit (one side may be omitted)") + slippage_pct: Optional[Decimal] = Field(default=None, description="Max acceptable slippage percentage") + + +class CLMMQuotePositionResponse(BaseModel): + """Gateway's position quote: the deposit split the pool would actually take.""" + base_limited: bool = Field(alias="baseLimited", description="True when the base side limits the deposit") + base_token_amount: Decimal = Field(alias="baseTokenAmount", description="Base amount the position would take") + quote_token_amount: Decimal = Field(alias="quoteTokenAmount", description="Quote amount the position would take") + base_token_amount_max: Decimal = Field(alias="baseTokenAmountMax", description="Base ceiling after slippage") + quote_token_amount_max: Decimal = Field(alias="quoteTokenAmountMax", description="Quote ceiling after slippage") + + model_config = {"populate_by_name": True} + + +class CLMMCreatePoolRequest(BaseModel): + """Request to create a new (empty) CLMM pool — liquidity is added by opening positions. + + Mirrors Gateway's POST /trading/clmm/create-pool. Connector extras are consumed + only by their owning connector. + """ + connector: str = Field(description="CLMM connector (e.g., 'meteora', 'raydium', 'orca', 'uniswap')") + network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta')") + base_token: str = Field(description="Base token symbol or address") + quote_token: str = Field(description="Quote token symbol or address") + initial_price: Optional[Decimal] = Field( + default=None, description="Initial price (quote per base); market price when omitted") + wallet_address: Optional[str] = Field(default=None, description="Wallet address (optional, uses default)") + extra_params: Optional[Dict[str, Any]] = Field( + default=None, + description="Connector-specific create params, passed through to Gateway under its own " + "names: binStep (meteora, orca), feeBps (meteora; required for uniswap/pancakeswap — the " + "V3 fee tier in basis points), ammConfigIndex (raydium, pancakeswap-sol). " + "Unknown keys are rejected.") + + class CLMMPositionsOwnedRequest(BaseModel): - """Request to get all CLMM positions owned by a wallet for a specific pool""" + """Request to get all CLMM positions owned by a wallet. + + Mirrors Gateway's /trading/clmm/positions-owned, which takes no pool filter — + every CLMM position the wallet owns on the connector is returned, each row + carrying its own pool_address. + """ connector: str = Field(description="CLMM connector (e.g., 'meteora', 'raydium', 'uniswap')") network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta')") - pool_address: str = Field(description="Pool contract address to filter positions") wallet_address: Optional[str] = Field(default=None, description="Wallet address (optional, uses default if not provided)") class CLMMPositionInfo(BaseModel): - """Information about a CLMM liquidity position""" + """Information about a CLMM liquidity position. + + Note: in_range here is a bool (live Gateway read); the DB-backed + /clmm/positions/search endpoint reports in_range as the string enum + IN_RANGE / OUT_OF_RANGE / UNKNOWN (three states, so not collapsible to bool). + """ position_address: str = Field(description="Position address") pool_address: str = Field(description="Pool address") - trading_pair: str = Field(description="Trading pair") - base_token: str = Field(description="Base token symbol") - quote_token: str = Field(description="Quote token symbol") + trading_pair: str = Field(description="Trading pair (address-derived identifiers, not symbols)") + base_token: str = Field(description="Base token identifier (derived from the token address; not a symbol)") + quote_token: str = Field(description="Quote token identifier (derived from the token address; not a symbol)") base_token_amount: Decimal = Field(description="Base token amount in position") quote_token_amount: Decimal = Field(description="Quote token amount in position") current_price: Decimal = Field(description="Current pool price") @@ -163,20 +299,6 @@ class CLMMPositionInfo(BaseModel): in_range: bool = Field(description="Whether position is currently in range") -class CLMMGetPositionInfoRequest(BaseModel): - """Request to get detailed info about a specific CLMM position""" - connector: str = Field(description="CLMM connector (e.g., 'meteora', 'raydium', 'uniswap')") - network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta')") - position_address: str = Field(description="Position address to query") - - -class CLMMPoolInfoRequest(BaseModel): - """Request to get CLMM pool information by pool address""" - connector: str = Field(description="CLMM connector (e.g., 'meteora', 'raydium')") - network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta')") - pool_address: str = Field(description="Pool contract address") - - class CLMMPoolBin(BaseModel): """Individual bin in a CLMM pool (e.g., Meteora)""" bin_id: int = Field(alias="binId", description="Bin identifier") @@ -207,10 +329,10 @@ class CLMMPoolInfoResponse(BaseModel): price: Decimal = Field(description="Current pool price") base_token_amount: Decimal = Field(alias="baseTokenAmount", description="Total base token liquidity") quote_token_amount: Decimal = Field(alias="quoteTokenAmount", description="Total quote token liquidity") - active_bin_id: Optional[int] = Field(None, alias="activeBinId", description="Currently active bin ID (Meteora DLMM only)") - dynamic_fee_pct: Optional[Decimal] = Field(None, alias="dynamicFeePct", description="Dynamic fee percentage") - min_bin_id: Optional[int] = Field(None, alias="minBinId", description="Minimum bin ID (Meteora-specific)") - max_bin_id: Optional[int] = Field(None, alias="maxBinId", description="Maximum bin ID (Meteora-specific)") + active_bin_id: Optional[int] = Field(None, alias="activeBinId", description="Currently active bin/tick ID") + # No dynamicFeePct/minBinId/maxBinId: those are Meteora connector extensions that + # Gateway's unified /trading/clmm/pool-info response schema strips before serialization, + # so they can never arrive here — and nothing downstream consumes them. bins: List[CLMMPoolBin] = Field(default_factory=list, description="List of bins with liquidity") model_config = { @@ -226,9 +348,6 @@ class CLMMPoolInfoResponse(BaseModel): "base_token_amount": 8645709.142366, "quote_token_amount": 1095942.335132, "active_bin_id": -374, - "dynamic_fee_pct": 0.2, - "min_bin_id": -21835, - "max_bin_id": 21835, "bins": [] } } @@ -283,45 +402,6 @@ class AMMPositionInfoResponse(BaseModel): model_config = {"populate_by_name": True} -class AMMQuoteSwapRequest(BaseModel): - """Request to quote a swap against a specific AMM pool.""" - connector: str = Field(description="AMM connector (e.g., 'meteora', 'raydium', 'uniswap')") - network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta')") - pool_address: str = Field(description="Pool contract address") - base_token: str = Field(description="Token that defines the swap direction (symbol or address)") - side: str = Field(description="Trade direction: BUY or SELL") - amount: Decimal = Field(description="Amount to swap (of base for SELL, of base to receive for BUY)") - slippage_pct: Optional[Decimal] = Field(default=None, description="Maximum slippage percentage") - - -class AMMQuoteSwapResponse(BaseModel): - """Response with an AMM swap quote.""" - pool_address: str = Field(alias="poolAddress", description="Pool address") - token_in: str = Field(alias="tokenIn", description="Input token address") - token_out: str = Field(alias="tokenOut", description="Output token address") - amount_in: Decimal = Field(alias="amountIn", description="Input amount") - amount_out: Decimal = Field(alias="amountOut", description="Output amount") - price: Decimal = Field(description="Execution price") - min_amount_out: Decimal = Field(alias="minAmountOut", description="Minimum output after slippage") - max_amount_in: Decimal = Field(alias="maxAmountIn", description="Maximum input after slippage") - price_impact_pct: Decimal = Field(alias="priceImpactPct", description="Price impact percentage") - slippage_pct: Optional[Decimal] = Field(default=None, alias="slippagePct", description="Slippage percentage used") - - model_config = {"populate_by_name": True} - - -class AMMExecuteSwapRequest(BaseModel): - """Request to execute a swap against a specific AMM pool.""" - connector: str = Field(description="AMM connector (e.g., 'meteora', 'raydium', 'uniswap')") - network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta')") - pool_address: str = Field(description="Pool contract address") - base_token: str = Field(description="Token that defines the swap direction (symbol or address)") - side: str = Field(description="Trade direction: BUY or SELL") - amount: Decimal = Field(description="Amount to swap") - slippage_pct: Optional[Decimal] = Field(default=1.0, description="Maximum slippage percentage (default: 1.0)") - wallet_address: Optional[str] = Field(default=None, description="Wallet address (optional, uses default)") - - class AMMTransactionResponse(BaseModel): """Chain-neutral write response. `signature` holds the tx signature (Solana) or tx hash (EVM).""" signature: str = Field(description="Transaction signature (Solana) or transaction hash (EVM)") @@ -359,7 +439,8 @@ class AMMAddLiquidityRequest(BaseModel): pool_address: str = Field(description="Pool contract address") base_token_amount: Decimal = Field(description="Amount of base token to add") quote_token_amount: Decimal = Field(description="Amount of quote token to add") - slippage_pct: Optional[Decimal] = Field(default=1.0, description="Maximum slippage percentage (default: 1.0)") + slippage_pct: Optional[Decimal] = Field( + default=None, description="Maximum slippage percentage; omit to use the connector's configured slippagePct") wallet_address: Optional[str] = Field(default=None, description="Wallet address (optional, uses default)") # Meteora DAMM v2: add to this specific NFT position; omit to open a NEW position. Ignored by fungible-LP AMMs. position_address: Optional[str] = Field(default=None, description="Meteora position to add to (omit = new position)") @@ -386,12 +467,16 @@ class AMMCreatePoolRequest(BaseModel): base_token_amount: Decimal = Field(description="Amount of base token to seed the pool with") quote_token_amount: Optional[Decimal] = Field(default=None, description="Amount of quote to seed (sets price if given)") initial_price: Optional[Decimal] = Field(default=None, description="Initial price (quote per base); overrides quote amount") + slippage_pct: Optional[Decimal] = Field( + default=None, + description="Seeding slippage percentage (uniswap/pancakeswap only); " + "omit to use the connector's configured slippagePct") wallet_address: Optional[str] = Field(default=None, description="Wallet address (optional, uses default)") - # Connector-specific create-pool extras (only consumed by their owning connector): - config_address: Optional[str] = Field(default=None, description="Meteora DAMM v2 config account (required for meteora)") - fee_config_index: Optional[int] = Field(default=None, description="Raydium CPMM fee config index (optional)") - gas_price: Optional[Decimal] = Field(default=None, description="Uniswap (EVM) gas price in gwei (optional)") - max_gas: Optional[int] = Field(default=None, description="Uniswap (EVM) max gas limit (optional)") + extra_params: Optional[Dict[str, Any]] = Field( + default=None, + description="Connector-specific create params, passed through to Gateway under its own " + "names: configAddress (meteora DAMM v2, required there), ammConfigIndex (raydium CPMM). " + "Unknown keys are rejected.") class AMMCreatePoolResponse(BaseModel): @@ -412,47 +497,10 @@ class AMMPositionsOwnedRequest(BaseModel): wallet_address: Optional[str] = Field(default=None, description="Wallet address (optional, uses default)") -# ============================================ -# Pool Information Models -# ============================================ - -class GetPoolInfoRequest(BaseModel): - """Request to get pool information""" - connector: str = Field(description="DEX connector (e.g., 'meteora', 'raydium', 'jupiter')") - network: str = Field(description="Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta')") - trading_pair: str = Field(description="Trading pair (e.g., 'SOL-USDC')") - - -class PoolInfo(BaseModel): - """Information about a liquidity pool""" - type: str = Field(description="Pool type: 'clmm' or 'router'") - address: str = Field(description="Pool address") - trading_pair: str = Field(description="Trading pair") - base_token: str = Field(description="Base token symbol") - quote_token: str = Field(description="Quote token symbol") - current_price: Decimal = Field(description="Current pool price") - base_token_amount: Decimal = Field(description="Base token liquidity in pool") - quote_token_amount: Decimal = Field(description="Quote token liquidity in pool") - fee_pct: Decimal = Field(description="Pool fee percentage") - - # CLMM-specific - bin_step: Optional[int] = Field(default=None, description="Bin step (CLMM)") - active_bin_id: Optional[int] = Field(default=None, description="Active bin ID (CLMM)") - - # ============================================ # CLMM Pool Listing Models # ============================================ -class TimeBasedMetrics(BaseModel): - """Time-based metrics (volume, fees, fee-to-TVL ratio) for different time periods""" - min_30: Optional[Decimal] = Field(default=None, description="30 minute metric") - hour_1: Optional[Decimal] = Field(default=None, description="1 hour metric") - hour_2: Optional[Decimal] = Field(default=None, description="2 hour metric") - hour_4: Optional[Decimal] = Field(default=None, description="4 hour metric") - hour_12: Optional[Decimal] = Field(default=None, description="12 hour metric") - hour_24: Optional[Decimal] = Field(default=None, description="24 hour metric") - class CLMMPoolListItem(BaseModel): """Individual pool item in CLMM pool listing - matches Gateway fetch-pools response""" @@ -476,4 +524,4 @@ class CLMMPoolListResponse(BaseModel): pools: List[CLMMPoolListItem] = Field(description="List of available pools") total: int = Field(description="Total number of matching pools") page: int = Field(description="Current page number") - pageSize: int = Field(description="Number of pools per page") + page_size: int = Field(description="Number of pools per page") diff --git a/routers/executors.py b/routers/executors.py index 1110fd43..bffe1552 100644 --- a/routers/executors.py +++ b/routers/executors.py @@ -18,6 +18,7 @@ ExecutorFilterRequest, ExecutorLogsResponse, ExecutorsSummaryResponse, + OrphanedPositionsResponse, PerformanceReportResponse, PositionHoldResponse, PositionsSummaryResponse, @@ -357,6 +358,57 @@ async def stop_executor( # Position Hold Endpoints # ======================================== +@router.get("/positions/orphaned", response_model=OrphanedPositionsResponse) +async def get_orphaned_positions( + executor_service: ExecutorService = Depends(get_executor_service) +): + """ + List terminated executors that may still own an on-chain position. + + Covers three orphan classes: + - Involuntary holds: close_type POSITION_HOLD with hold_reason set (an LP close + that exhausted its retries): the position is live on-chain with no automated + owner. + - Legacy FAILED records whose final state still reported a position_address + (force-stop stragglers, records persisted by older executors). + - Executors terminated by SYSTEM_CLEANUP after an API restart: their on-chain + state was never persisted, so they need external reconciliation. + + This is a DB-side listing. Before recovering, cross-check candidates against + on-chain reality via the gateway positions-owned endpoints + (/trading/clmm/positions-owned, /trading/amm/positions-owned). + """ + try: + orphans = await executor_service.get_orphaned_positions() + return OrphanedPositionsResponse(count=len(orphans), orphans=orphans) + except Exception as e: + logger.error(f"Error listing orphaned positions: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Error listing orphaned positions: {str(e)}") + + +@router.post("/{executor_id}/resolve-orphan") +async def resolve_orphaned_position( + executor_id: str, + executor_service: ExecutorService = Depends(get_executor_service) +): + """ + Mark an orphaned position as recovered. + + Call after the stranded on-chain position has been closed (or adopted) + externally. Removes the executor from /executors/positions/orphaned and from + agent-facing orphan warnings. Only valid for terminated executors that are + orphan candidates: an involuntary hold (POSITION_HOLD with hold_reason or the + orphaned_position flag), FAILED, or SYSTEM_CLEANUP. + """ + try: + return await executor_service.resolve_orphaned_position(executor_id) + except HTTPException: + raise + except Exception as e: + logger.error(f"Error resolving orphaned position for {executor_id}: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Error resolving orphaned position: {str(e)}") + + @router.get("/positions/summary", response_model=PositionsSummaryResponse) async def get_positions_summary( controller_id: Optional[str] = None, diff --git a/routers/gateway.py b/routers/gateway.py index ec5f4419..cab5cbc7 100644 --- a/routers/gateway.py +++ b/routers/gateway.py @@ -306,8 +306,14 @@ async def update_api_keys( results = await accounts_service.gateway_client.update_api_keys(request.api_keys) - # Check for any errors in the results - errors = [r for r in results if r and "error" in r] + # A None result means the request never reached Gateway (connection + # error mid-batch) — that is a failure, not a success to filter out. + if any(r is None for r in results): + raise HTTPException( + status_code=503, + detail="Gateway became unreachable while updating API keys; not all keys were applied", + ) + errors = [r for r in results if "error" in r] if errors: raise HTTPException(status_code=400, detail=f"Failed to update some API keys: {errors}") @@ -369,9 +375,11 @@ async def list_pools_legacy( if not await accounts_service.gateway_client.ping(): raise HTTPException(status_code=503, detail="Gateway service is not available") - # Determine chain from connector (legacy behavior) - # This is a simple mapping - in production, you'd want to look this up - chain = "solana" if connector_name in ["raydium", "meteora", "orca", "pancakeswap-sol"] else "ethereum" + # Determine chain from connector (legacy behavior). Solana routers + # (jupiter/dflow/okx/titan) must map to solana too, or this returns the + # (empty) ethereum pool list for them. + solana_connectors = {"raydium", "meteora", "orca", "pancakeswap-sol", "jupiter", "dflow", "okx", "titan"} + chain = "solana" if connector_name in solana_connectors else "ethereum" pools = check_gateway_error( await accounts_service.gateway_client.get_pools(chain, network, connector=connector_name) diff --git a/routers/gateway_amm.py b/routers/gateway_amm.py index 42d9d0d9..607c1a4f 100644 --- a/routers/gateway_amm.py +++ b/routers/gateway_amm.py @@ -21,17 +21,15 @@ AMMAddLiquidityRequest, AMMCreatePoolRequest, AMMCreatePoolResponse, - AMMExecuteSwapRequest, AMMPoolInfoResponse, AMMPositionInfoResponse, AMMPositionsOwnedRequest, AMMQuoteLiquidityRequest, AMMQuoteLiquidityResponse, - AMMQuoteSwapRequest, - AMMQuoteSwapResponse, AMMRemoveLiquidityRequest, AMMTransactionResponse, ) +from routers.gateway_extras import ExtraParamsSpec, validate_extra_params from services.accounts_service import AccountsService from services.gateway_client import GatewayError, check_gateway_error @@ -39,6 +37,14 @@ router = APIRouter(tags=["Gateway AMM"], prefix="/gateway") +# Gateway's unified create-pool destructure, per consuming connector: +# configAddress (meteora DAMM v2 — required there), ammConfigIndex (raydium CPMM). +# EVM seeding slippage is the standard slippage_pct field, not an extra param. +AMM_CREATE_POOL_EXTRA_PARAMS_SPEC: ExtraParamsSpec = { + "configAddress": ((str,), {"meteora"}), + "ammConfigIndex": ((int,), {"raydium"}), +} + async def _require_gateway(accounts_service: AccountsService) -> None: if not await accounts_service.gateway_client.ping(): @@ -137,31 +143,6 @@ async def get_amm_positions_owned( raise HTTPException(status_code=500, detail=f"Error getting AMM positions owned: {str(e)}") -@router.post("/amm/quote-swap", response_model=AMMQuoteSwapResponse, response_model_by_alias=False) -async def quote_amm_swap( - request: AMMQuoteSwapRequest, - accounts_service: AccountsService = Depends(get_accounts_service), -): - """Quote a swap against a specific AMM pool (pool-scoped, not router).""" - try: - await _require_gateway(accounts_service) - result = check_gateway_error(await accounts_service.gateway_client.amm_quote_swap( - connector=request.connector, chain_network=request.network, pool_address=request.pool_address, - base_token=request.base_token, side=request.side, amount=float(request.amount), - slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else None, - )) - return AMMQuoteSwapResponse(**result) - except HTTPException: - raise - except GatewayError as e: - raise HTTPException(status_code=e.status, detail=f"Gateway error quoting AMM swap: {e}") - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - except Exception as e: - logger.error(f"Error quoting AMM swap: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"Error quoting AMM swap: {str(e)}") - - @router.post("/amm/quote-liquidity", response_model=AMMQuoteLiquidityResponse, response_model_by_alias=False) async def quote_amm_liquidity( request: AMMQuoteLiquidityRequest, @@ -189,33 +170,6 @@ async def quote_amm_liquidity( # ----------------------------- Writes ----------------------------- -@router.post("/amm/execute-swap", response_model=AMMTransactionResponse) -async def execute_amm_swap( - request: AMMExecuteSwapRequest, - accounts_service: AccountsService = Depends(get_accounts_service), -): - """Execute a swap against a specific AMM pool.""" - try: - await _require_gateway(accounts_service) - wallet_address = await _resolve_wallet(accounts_service, request.network, request.wallet_address) - result = check_gateway_error(await accounts_service.gateway_client.amm_execute_swap( - connector=request.connector, chain_network=request.network, wallet_address=wallet_address, - pool_address=request.pool_address, base_token=request.base_token, side=request.side, - amount=float(request.amount), - slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else None, - )) - return AMMTransactionResponse(**result) - except HTTPException: - raise - except GatewayError as e: - raise HTTPException(status_code=e.status, detail=f"Gateway error executing AMM swap: {e}") - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - except Exception as e: - logger.error(f"Error executing AMM swap: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"Error executing AMM swap: {str(e)}") - - @router.post("/amm/add-liquidity", response_model=AMMTransactionResponse) async def add_amm_liquidity( request: AMMAddLiquidityRequest, @@ -290,10 +244,21 @@ async def create_amm_pool( Create and seed a new AMM pool. Seed price priority: initial_price → quote_token_amount ratio → live market price (anti-snipe). - Connector extras are sent only when provided (config_address for meteora, fee_config_index for - raydium, gas_price/max_gas for uniswap). + Connector-specific params ride extra_params under Gateway's own names (configAddress for + meteora — required there, ammConfigIndex for raydium) — the same contract as clmm open's + extra_params. Seeding slippage for uniswap/pancakeswap is the standard slippage_pct field. """ try: + validate_extra_params(request.extra_params, AMM_CREATE_POOL_EXTRA_PARAMS_SPEC, + request.connector, "unified /trading/amm/create-pool") + extra_params = request.extra_params or {} + if request.connector == "meteora" and not extra_params.get("configAddress"): + raise HTTPException( + status_code=400, + detail="extra_params.configAddress is required for meteora create-pool " + "(DAMM v2 pools are created against a config account)." + ) + await _require_gateway(accounts_service) wallet_address = await _resolve_wallet(accounts_service, request.network, request.wallet_address) result = check_gateway_error(await accounts_service.gateway_client.amm_create_pool( @@ -302,10 +267,8 @@ async def create_amm_pool( base_token_amount=float(request.base_token_amount), quote_token_amount=float(request.quote_token_amount) if request.quote_token_amount is not None else None, initial_price=float(request.initial_price) if request.initial_price is not None else None, - config_address=request.config_address, - fee_config_index=request.fee_config_index, - gas_price=float(request.gas_price) if request.gas_price is not None else None, - max_gas=request.max_gas, + slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else None, + extra_params=request.extra_params, )) return AMMCreatePoolResponse(**result) except HTTPException: diff --git a/routers/gateway_clmm.py b/routers/gateway_clmm.py index 7e9c92ec..211ccc5f 100644 --- a/routers/gateway_clmm.py +++ b/routers/gateway_clmm.py @@ -7,17 +7,19 @@ from decimal import Decimal from typing import List, Optional -import aiohttp from fastapi import APIRouter, Depends, HTTPException, Query from database import AsyncDatabaseManager from database.repositories import GatewayCLMMRepository from deps import get_accounts_service, get_database_manager from models import ( + AMMCreatePoolResponse, CLMMAddLiquidityRequest, CLMMClosePositionRequest, + CLMMClosePositionResponse, CLMMCollectFeesRequest, CLMMCollectFeesResponse, + CLMMCreatePoolRequest, CLMMOpenPositionRequest, CLMMOpenPositionResponse, CLMMPoolInfoResponse, @@ -25,149 +27,32 @@ CLMMPoolListResponse, CLMMPositionInfo, CLMMPositionsOwnedRequest, + CLMMQuotePositionRequest, + CLMMQuotePositionResponse, CLMMRemoveLiquidityRequest, - TimeBasedMetrics, ) +from routers.gateway_extras import ExtraParamsSpec, get_transaction_status_from_response, validate_extra_params from services.accounts_service import AccountsService -from services.gateway_client import GatewayError, check_gateway_error +from services.gateway_client import GatewayError, check_gateway_error, get_native_gas_token logger = logging.getLogger(__name__) router = APIRouter(tags=["Gateway CLMM"], prefix="/gateway") +# Gateway's unified open/add destructure ONLY strategyType, and only Meteora +# consumes it (Spot=0 / Curve=1). +CLMM_LIQUIDITY_EXTRA_PARAMS_SPEC: ExtraParamsSpec = { + "strategyType": ((int,), {"meteora"}), +} -async def fetch_raydium_pool_info(pool_address: str) -> Optional[dict]: - """ - Fetch pool info from Raydium API. - - Args: - pool_address: Pool contract address - - Returns: - Dictionary with pool info from Raydium API, or None if failed - """ - try: - url = f"https://api-v3.raydium.io/pools/info/ids?ids={pool_address}" - async with aiohttp.ClientSession() as session: - async with session.get(url, headers={"accept": "application/json"}) as response: - response.raise_for_status() - data = await response.json() - - if not data.get("success"): - logger.error(f"Raydium API returned unsuccessful response: {data}") - return None - - # Extract the first pool from the data list - pools_data = data.get("data", []) - if not pools_data: - logger.error(f"Raydium API returned empty data for pool: {pool_address}") - return None - - # Return the pool data directly (not wrapped in data key) - return pools_data[0] - except aiohttp.ClientError as e: - logger.error(f"Failed to fetch pool info from Raydium API: {e}") - return None - except Exception as e: - logger.error(f"Error fetching Raydium pool info: {e}", exc_info=True) - return None - - -def transform_raydium_to_clmm_response(raydium_data: dict, pool_address: str) -> dict: - """ - Transform Raydium API response to match Gateway's CLMMPoolInfoResponse format. - - Args: - raydium_data: Pool data from Raydium API (pools/info/ids endpoint) - pool_address: Pool contract address - - Returns: - Dictionary matching Gateway's pool info structure - """ - # Extract token info - mint_a = raydium_data.get("mintA", {}) - mint_b = raydium_data.get("mintB", {}) - - base_token_address = mint_a.get("address", "") - quote_token_address = mint_b.get("address", "") - - # Get current price - current_price = Decimal(str(raydium_data.get("price", 0))) - - # Get token amounts - base_amount = Decimal(str(raydium_data.get("mintAmountA", 0))) - quote_amount = Decimal(str(raydium_data.get("mintAmountB", 0))) - - # Get fee rate (convert from decimal to percentage, e.g., 0.0025 -> 0.25%) - fee_rate = raydium_data.get("feeRate", 0.0025) - fee_pct = Decimal(str(fee_rate * 100)) - - # Check if this is a CLMM (Concentrated) pool - pool_type = raydium_data.get("type", "Standard") - is_clmm = pool_type == "Concentrated" - - # Return in Gateway-compatible format - return { - "address": pool_address, - "baseTokenAddress": base_token_address, - "quoteTokenAddress": quote_token_address, - "binStep": 1 if is_clmm else None, # CLMM pools have tick spacing - "feePct": fee_pct, - "price": current_price, - "baseTokenAmount": base_amount, - "quoteTokenAmount": quote_amount, - "activeBinId": None, # Not available from this endpoint - "dynamicFeePct": None, - "minBinId": None, - "maxBinId": None, - "bins": [] # Bin data not available from pool info endpoint - } - - -def get_transaction_status_from_response(gateway_response: dict) -> str: - """ - Determine transaction status from Gateway response. - - Gateway returns status field in the response: - - status: 1 = confirmed - - status: 0 = pending/submitted - - Returns: - "CONFIRMED" if status == 1 - "SUBMITTED" if status == 0 or not present - """ - status = gateway_response.get("status") - - # Status 1 means transaction is confirmed on-chain - if status == 1: - return "CONFIRMED" - - # Status 0 or missing means submitted but not confirmed yet - return "SUBMITTED" - - -def get_native_gas_token(chain: str) -> str: - """ - Get the native gas token symbol for a blockchain. - - Args: - chain: Blockchain name (e.g., 'solana', 'ethereum', 'polygon') - - Returns: - Gas token symbol (e.g., 'SOL', 'ETH', 'MATIC') - """ - gas_token_map = { - "solana": "SOL", - "ethereum": "ETH", - "polygon": "MATIC", - "avalanche": "AVAX", - "optimism": "ETH", - "arbitrum": "ETH", - "base": "ETH", - "bsc": "BNB", - "cronos": "CRO", - } - return gas_token_map.get(chain.lower(), "UNKNOWN") +# Gateway's unified create-pool destructure, per consuming connector: binStep +# (meteora bin step / orca tick spacing), feeBps (meteora; required V3 fee tier +# for uniswap/pancakeswap), ammConfigIndex (raydium, pancakeswap-sol). +CLMM_CREATE_POOL_EXTRA_PARAMS_SPEC: ExtraParamsSpec = { + "binStep": ((int,), {"meteora", "orca"}), + "feeBps": ((int, float), {"meteora", "uniswap", "pancakeswap"}), + "ammConfigIndex": ((int,), {"raydium", "pancakeswap-sol"}), +} async def _refresh_position_data(position, accounts_service: AccountsService, clmm_repo: GatewayCLMMRepository): @@ -191,8 +76,7 @@ async def _refresh_position_data(position, accounts_service: AccountsService, cl positions_list = check_gateway_error(await accounts_service.gateway_client.clmm_positions_owned( connector=position.connector, chain_network=position.network, # position.network is already in 'chain-network' format - wallet_address=wallet_address, - pool_address=position.pool_address + wallet_address=wallet_address )) # Find our specific position in the list @@ -203,10 +87,13 @@ async def _refresh_position_data(position, accounts_service: AccountsService, cl result = pos break - # If position not found, it was closed externally + # Absent from a single positions-owned read: could be closed externally, + # could be a lagging RPC node. Closing is owned by the poller's + # consecutive-miss gate (and the zero-liquidity check below) so one + # refresh can never close a live position. if result is None: - logger.info(f"Position {position.position_address} not found on Gateway, marking as CLOSED") - await clmm_repo.close_position(position.position_address) + logger.info(f"Position {position.position_address} absent from positions-owned; " + "skipping update (poller's miss-gate owns close detection)") return except Exception as e: @@ -246,19 +133,19 @@ async def _refresh_position_data(position, accounts_service: AccountsService, cl current_price=current_price ) - # Update pending fees if available + # Always write pending fees — 0 is a real value (e.g. right after an + # external collect); the old non-zero guard left stale pendings forever. base_fee_pending = Decimal(str(result.get("baseFeeAmount", 0))) quote_fee_pending = Decimal(str(result.get("quoteFeeAmount", 0))) - if base_fee_pending or quote_fee_pending: - await clmm_repo.update_position_fees( - position_address=position.position_address, - base_fee_pending=base_fee_pending, - quote_fee_pending=quote_fee_pending - ) + await clmm_repo.update_position_fees( + position_address=position.position_address, + base_fee_pending=base_fee_pending, + quote_fee_pending=quote_fee_pending + ) logger.debug(f"Refreshed position {position.position_address}: price={current_price}, in_range={in_range}, " - f"base={base_token_amount}, quote={quote_token_amount}") + f"base={base_token_amount}, quote={quote_token_amount}") except Exception as e: logger.error(f"Error refreshing position {position.position_address}: {e}", exc_info=True) @@ -270,6 +157,7 @@ async def get_clmm_pool_info( connector: str, network: str, pool_address: str, + bin_count: int = 0, accounts_service: AccountsService = Depends(get_accounts_service) ): """ @@ -279,43 +167,20 @@ async def get_clmm_pool_info( connector: CLMM connector (e.g., 'meteora', 'raydium') network: Network ID in 'chain-network' format (e.g., 'solana-mainnet-beta') pool_address: Pool contract address + bin_count: If > 0, include the per-tick liquidity distribution (`bins`) + around the active tick. Meteora always returns its bins and ignores + this; orca, raydium, uniswap and pancakeswap honour it. Example: - GET /gateway/clmm/pool-info?connector=meteora&network=solana-mainnet-beta&pool_address=2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3 + GET /gateway/clmm/pool-info?connector=meteora&network=solana-mainnet-beta + &pool_address=2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3 Returns: Pool information including liquidity, price, bins (for Meteora), etc. All field names are returned in snake_case format. - Note: - For Raydium connector, uses Raydium API directly instead of Gateway. """ try: - # Special handling for Raydium - use Raydium API directly (not Gateway) - if connector.lower() == "raydium": - logger.info(f"Using Raydium API directly for pool info: {pool_address}") - - # Fetch from Raydium API - raydium_data = await fetch_raydium_pool_info(pool_address) - if raydium_data is None: - raise HTTPException(status_code=503, detail="Failed to get pool info from Raydium API") - - # Check if this is a CLMM pool - Standard AMM pools are not supported on this endpoint - pool_type = raydium_data.get("type", "Standard") - if pool_type != "Concentrated": - raise HTTPException( - status_code=400, - detail=f"Pool {pool_address} is a Raydium {pool_type} AMM pool, not a CLMM pool. " - f"This endpoint only supports Concentrated Liquidity (CLMM) pools." - ) - - # Transform to Gateway-compatible format - result = transform_raydium_to_clmm_response(raydium_data, pool_address) - - # Parse into response model - return CLMMPoolInfoResponse(**result) - - # Default behavior for other connectors: use Gateway if not await accounts_service.gateway_client.ping(): raise HTTPException(status_code=503, detail="Gateway service is not available") @@ -323,7 +188,8 @@ async def get_clmm_pool_info( result = check_gateway_error(await accounts_service.gateway_client.clmm_pool_info( connector=connector, chain_network=network, - pool_address=pool_address + pool_address=pool_address, + bin_count=bin_count )) # Parse the camelCase Gateway response into snake_case Pydantic model @@ -344,6 +210,9 @@ async def get_clmm_pool_info( @router.get("/clmm/pools", response_model=CLMMPoolListResponse) async def get_clmm_pools( connector: str, + network: str = Query( + "mainnet-beta", + description="Solana network name (bare, e.g. 'mainnet-beta'); meteora/orca are Solana-only"), page: int = Query(0, ge=0, description="Page number"), limit: int = Query(50, ge=1, le=100, description="Results per page (max 100)"), search_term: Optional[str] = Query(None, description="Search query to filter pools"), @@ -359,6 +228,7 @@ async def get_clmm_pools( Args: connector: CLMM connector (meteora, orca) + network: Solana network name (bare, default 'mainnet-beta') page: Page number (default: 0) limit: Results per page (default: 50, max: 100) search_term: Search query to filter pools (optional) @@ -367,7 +237,7 @@ async def get_clmm_pools( include_unknown: Include pools with unverified tokens Example: - GET /gateway/clmm/pools?connector=meteora&query=SOL&limit=20 + GET /gateway/clmm/pools?connector=meteora&search_term=SOL&limit=20 Returns: List of available pools with trading pairs, addresses, liquidity, volume, APR, etc. @@ -382,25 +252,37 @@ async def get_clmm_pools( logger.info(f"Fetching pools from Gateway ({connector}, page={page}, limit={limit}, query={search_term})") - # Build sort_by for Gateway (connector-specific format) - sort_by = None - if sort_key: - if connector.lower() == "meteora": - time_suffix = "_24h" if sort_key in ["volume", "fees"] else "" - direction = order_by if order_by else "desc" - sort_by = f"{sort_key}{time_suffix}:{direction}" - else: # orca - sort_by = sort_key - - gateway_data = check_gateway_error(await accounts_service.gateway_client.clmm_fetch_pools( - connector=connector.lower(), - network="mainnet-beta", - page=page, - limit=limit, - query=search_term, - sort_by=sort_by, - include_unverified=include_unknown - )) + # The two fetch-pools routes take different params: meteora paginates and + # filters via page/includeUnverified with "field:direction" sortBy; orca does + # not paginate and uses sortBy + sortDirection + verifiedOnly. + if connector.lower() == "meteora": + time_suffix = "_24h" if sort_key in ["volume", "fees"] else "" + direction = order_by if order_by else "desc" + gateway_data = check_gateway_error(await accounts_service.gateway_client.clmm_fetch_pools( + connector="meteora", + network=network, + limit=limit, + query=search_term, + sort_by=f"{sort_key}{time_suffix}:{direction}" if sort_key else None, + page=page, + include_unverified=include_unknown + )) + else: # orca + if page > 0: + raise HTTPException( + status_code=400, + detail="Orca's pool listing does not paginate; page is meteora-only. " + "Raise limit instead (max 100)." + ) + gateway_data = check_gateway_error(await accounts_service.gateway_client.clmm_fetch_pools( + connector="orca", + network=network, + limit=limit, + query=search_term, + sort_by=sort_key, + sort_direction=order_by, + verified_only=not include_unknown + )) # Transform Gateway response to our format # Both Meteora and Orca now return same format: {pools: [...], total, page, pageSize} @@ -431,7 +313,7 @@ async def get_clmm_pools( pools=pools, total=total, page=page, - pageSize=limit + page_size=limit ) except HTTPException: @@ -460,14 +342,19 @@ async def open_clmm_position( upper_price: 250 base_token_amount: 0.01 quote_token_amount: 2 - slippage_pct: 1 + slippage_pct: 1 (optional; omit to use the connector's configured slippagePct) wallet_address: (optional) extra_params: {"strategyType": 0} # Meteora-specific Returns: - Transaction hash and position address + Transaction hash and position address. position_address is None when the + transaction was submitted but not yet confirmed — poll the transaction; the + poller's discovery sweep records the position once it lands on-chain. """ try: + validate_extra_params(request.extra_params, CLMM_LIQUIDITY_EXTRA_PARAMS_SPEC, + request.connector, "unified /trading/clmm/open") + if not await accounts_service.gateway_client.ping(): raise HTTPException(status_code=503, detail="Gateway service is not available") @@ -512,25 +399,69 @@ async def open_clmm_position( upper_price=float(request.upper_price), base_token_amount=float(request.base_token_amount) if request.base_token_amount else None, quote_token_amount=float(request.quote_token_amount) if request.quote_token_amount else None, - slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else 1.0, + slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else None, extra_params=request.extra_params )) - transaction_hash = result.get("signature") or result.get("txHash") or result.get("hash") + transaction_hash = result.get("signature") + if not transaction_hash: + raise HTTPException(status_code=500, detail="No transaction signature returned from Gateway") - # Position address can be at root level or nested in data object - data = result.get("data", {}) - position_address = result.get("positionAddress") or result.get("position") or data.get("positionAddress") or data.get("position") + # Gateway's OpenPositionResponse carries position details only inside `data`, + # which is present only for CONFIRMED transactions (the response schema strips + # any other key, so there is no top-level fallback to read). + data = result.get("data") or {} + position_address = data.get("positionAddress") + tx_status = get_transaction_status_from_response(result) + + if not position_address: + if tx_status == "CONFIRMED": + raise HTTPException( + status_code=500, + detail="Gateway confirmed the open but returned no position address") + # data present without a position address is the EVM revert shape + # (uniswap/pancakeswap return the receipt with an empty address when the + # tx landed but reverted); a negative status is a terminal failure on any + # chain. Both are definitive failures, not pending submissions. + if tx_status == "FAILED" or result.get("data") is not None: + raise HTTPException( + status_code=500, + detail=f"Open position transaction failed on-chain ({transaction_hash})") + # Submitted-not-confirmed: the position address is unknowable until the tx + # lands. The tx IS in flight, so failing here would report a false failure + # and orphan the position; instead return the signature for the caller to + # poll — the transaction poller's discovery sweep records the position in + # the database once it appears on-chain. + logger.warning( + f"CLMM open submitted but not confirmed ({transaction_hash}); position address " + "unknown — the poller's discovery sweep will record the position once it lands") + return CLMMOpenPositionResponse( + transaction_hash=transaction_hash, + position_address=None, + trading_pair=trading_pair, + pool_address=request.pool_address, + lower_price=request.lower_price, + upper_price=request.upper_price, + base_token_amount_added=request.base_token_amount, + quote_token_amount_added=request.quote_token_amount, + position_rent=None, + status="submitted", + ) # Extract position rent (SOL locked for position NFT) position_rent = data.get("positionRent") if position_rent: logger.info(f"Position rent: {position_rent} SOL") - if not transaction_hash: - raise HTTPException(status_code=500, detail="No transaction hash returned from Gateway") - if not position_address: - raise HTTPException(status_code=500, detail="No position address returned from Gateway") + # CONFIRMED path: prefer the on-chain amounts over the requested ones — + # slippage and rounding make them differ, and persisting the request + # silently diverges the DB from the chain. + base_amount_added = data.get("baseTokenAmountAdded") + if base_amount_added is None: + base_amount_added = float(request.base_token_amount) if request.base_token_amount else 0 + quote_amount_added = data.get("quoteTokenAmountAdded") + if quote_amount_added is None: + quote_amount_added = float(request.quote_token_amount) if request.quote_token_amount else 0 # Calculate percentage: (upper_price - lower_price) / lower_price percentage = None @@ -538,9 +469,6 @@ async def open_clmm_position( percentage = float((request.upper_price - request.lower_price) / request.lower_price) logger.info(f"Position price range percentage: {percentage:.4f} ({percentage*100:.2f}%)") - # Get transaction status from Gateway response - tx_status = get_transaction_status_from_response(result) - # Extract gas fee from Gateway response gas_fee = data.get("fee") gas_token = get_native_gas_token(chain) @@ -566,11 +494,11 @@ async def open_clmm_position( "percentage": percentage, "entry_price": entry_price, # Pool price when position opened "current_price": entry_price, # Same as entry at open time, updated by poller - "initial_base_token_amount": float(request.base_token_amount) if request.base_token_amount else 0, - "initial_quote_token_amount": float(request.quote_token_amount) if request.quote_token_amount else 0, + "initial_base_token_amount": float(base_amount_added), + "initial_quote_token_amount": float(quote_amount_added), "position_rent": float(position_rent) if position_rent else None, - "base_token_amount": float(request.base_token_amount) if request.base_token_amount else 0, - "quote_token_amount": float(request.quote_token_amount) if request.quote_token_amount else 0, + "base_token_amount": float(base_amount_added), + "quote_token_amount": float(quote_amount_added), "in_range": "UNKNOWN" # Will be updated by poller } @@ -582,15 +510,16 @@ async def open_clmm_position( "position_id": position.id, "transaction_hash": transaction_hash, "event_type": "OPEN", - "base_token_amount": float(request.base_token_amount) if request.base_token_amount else None, - "quote_token_amount": float(request.quote_token_amount) if request.quote_token_amount else None, - "gas_fee": float(gas_fee) if gas_fee else None, + "base_token_amount": float(base_amount_added) if base_amount_added is not None else None, + "quote_token_amount": float(quote_amount_added) if quote_amount_added is not None else None, + "gas_fee": float(gas_fee) if gas_fee is not None else None, "gas_token": gas_token, "status": tx_status } await clmm_repo.create_event(event_data) - logger.info(f"Recorded CLMM OPEN event in database: {transaction_hash} (status: {tx_status}, gas: {gas_fee} {gas_token})") + logger.info(f"Recorded CLMM OPEN event in database: {transaction_hash} " + f"(status: {tx_status}, gas: {gas_fee} {gas_token})") except Exception as db_error: # Log but don't fail the operation - it was submitted successfully logger.error(f"Error recording CLMM position in database: {db_error}", exc_info=True) @@ -602,7 +531,10 @@ async def open_clmm_position( pool_address=request.pool_address, lower_price=request.lower_price, upper_price=request.upper_price, - status="submitted" + base_token_amount_added=Decimal(str(base_amount_added)) if base_amount_added is not None else None, + quote_token_amount_added=Decimal(str(quote_amount_added)) if quote_amount_added is not None else None, + position_rent=Decimal(str(position_rent)) if position_rent else None, + status="confirmed" ) except HTTPException: @@ -631,13 +563,17 @@ async def add_liquidity_to_clmm_position( position_address: '...' base_token_amount: 0.5 quote_token_amount: 50.0 - slippage_pct: 1 + slippage_pct: 1 (optional; omit to use the connector's configured slippagePct) wallet_address: (optional) + extra_params: {"strategyType": 0} # Meteora-specific Returns: Transaction hash """ try: + validate_extra_params(request.extra_params, CLMM_LIQUIDITY_EXTRA_PARAMS_SPEC, + request.connector, "unified /trading/clmm/add") + if not await accounts_service.gateway_client.ping(): raise HTTPException(status_code=503, detail="Gateway service is not available") @@ -658,7 +594,8 @@ async def add_liquidity_to_clmm_position( position_address=request.position_address, base_token_amount=float(request.base_token_amount) if request.base_token_amount else None, quote_token_amount=float(request.quote_token_amount) if request.quote_token_amount else None, - slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else 1.0 + slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else None, + extra_params=request.extra_params )) transaction_hash = result.get("signature") or result.get("txHash") or result.get("hash") @@ -671,7 +608,17 @@ async def add_liquidity_to_clmm_position( # Extract gas fee from Gateway response data = result.get("data", {}) gas_fee = data.get("fee") - gas_token = "SOL" if chain == "solana" else "ETH" if chain == "ethereum" else None + gas_token = get_native_gas_token(chain) + + # Prefer the CONFIRMED on-chain amounts (data is only present when Gateway + # confirmed the tx); the requested amounts are the submitted-not-confirmed + # fallback, reconciled later by the poller. + base_amount_added = data.get("baseTokenAmountAdded") + if base_amount_added is None: + base_amount_added = float(request.base_token_amount) if request.base_token_amount else None + quote_amount_added = data.get("quoteTokenAmountAdded") + if quote_amount_added is None: + quote_amount_added = float(request.quote_token_amount) if request.quote_token_amount else None # Store ADD_LIQUIDITY event in database try: @@ -685,21 +632,42 @@ async def add_liquidity_to_clmm_position( "position_id": position.id, "transaction_hash": transaction_hash, "event_type": "ADD_LIQUIDITY", - "base_token_amount": float(request.base_token_amount) if request.base_token_amount else None, - "quote_token_amount": float(request.quote_token_amount) if request.quote_token_amount else None, - "gas_fee": float(gas_fee) if gas_fee else None, + # `is not None`: 0 is a real amount on single-sided adds + "base_token_amount": float(base_amount_added) if base_amount_added is not None else None, + "quote_token_amount": float(quote_amount_added) if quote_amount_added is not None else None, + "gas_fee": float(gas_fee) if gas_fee is not None else None, "gas_token": gas_token, "status": tx_status } await clmm_repo.create_event(event_data) - logger.info(f"Recorded CLMM ADD_LIQUIDITY event: {transaction_hash} (status: {tx_status}, gas: {gas_fee} {gas_token})") + logger.info(f"Recorded CLMM ADD_LIQUIDITY event: {transaction_hash} " + f"(status: {tx_status}, gas: {gas_fee} {gas_token})") + + # Added capital raises the PnL baseline. Book here only when the + # tx confirmed inline (the event is created CONFIRMED and the + # poller never re-processes it); SUBMITTED events are booked by + # the poller's confirm path. + if tx_status == "CONFIRMED": + await clmm_repo.add_to_initial_amounts( + position_address=request.position_address, + base_delta=Decimal(str(base_amount_added or 0)), + quote_delta=Decimal(str(quote_amount_added or 0)), + ) + else: + logger.warning(f"ADD_LIQUIDITY {transaction_hash} executed for position " + f"{request.position_address} with no database record — " + "no event recorded (position may be a pending open " + "not yet discovered)") except Exception as db_error: logger.error(f"Error recording ADD_LIQUIDITY event: {db_error}", exc_info=True) return { "transaction_hash": transaction_hash, "position_address": request.position_address, - "status": "submitted" + "base_token_amount_added": base_amount_added, + "quote_token_amount_added": quote_amount_added, + "gas_fee": gas_fee, + "status": tx_status.lower() } except HTTPException: @@ -726,7 +694,8 @@ async def remove_liquidity_from_clmm_position( connector: 'meteora' network: 'solana-mainnet-beta' position_address: '...' - percentage: 50 + percentage_to_remove: 50 + slippage_pct: 1 (optional; Orca only — other connectors ignore it) wallet_address: (optional) Returns: @@ -751,7 +720,8 @@ async def remove_liquidity_from_clmm_position( chain_network=request.network, wallet_address=wallet_address, position_address=request.position_address, - percentage=float(request.percentage) + percentage_to_remove=float(request.percentage_to_remove), + slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else None )) transaction_hash = result.get("signature") or result.get("txHash") or result.get("hash") @@ -764,7 +734,12 @@ async def remove_liquidity_from_clmm_position( # Extract gas fee from Gateway response data = result.get("data", {}) gas_fee = data.get("fee") - gas_token = "SOL" if chain == "solana" else "ETH" if chain == "ethereum" else None + gas_token = get_native_gas_token(chain) + + # The CONFIRMED on-chain amounts (data is only present when Gateway confirmed + # the tx). A percentage alone says nothing about what actually left the pool. + base_amount_removed = data.get("baseTokenAmountRemoved") + quote_amount_removed = data.get("quoteTokenAmountRemoved") # Store REMOVE_LIQUIDITY event in database try: @@ -774,25 +749,38 @@ async def remove_liquidity_from_clmm_position( # Get position to link event position = await clmm_repo.get_position_by_address(request.position_address) if position: + # No "percentage" key: GatewayCLMMEvent has no such column, and the + # stray kwarg made create_event raise — silently losing every + # REMOVE_LIQUIDITY event to the log-and-continue handler below. event_data = { "position_id": position.id, "transaction_hash": transaction_hash, "event_type": "REMOVE_LIQUIDITY", - "percentage": float(request.percentage), - "gas_fee": float(gas_fee) if gas_fee else None, + "base_token_amount": float(base_amount_removed) if base_amount_removed is not None else None, + "quote_token_amount": float(quote_amount_removed) if quote_amount_removed is not None else None, + "gas_fee": float(gas_fee) if gas_fee is not None else None, "gas_token": gas_token, "status": tx_status } await clmm_repo.create_event(event_data) - logger.info(f"Recorded CLMM REMOVE_LIQUIDITY event: {transaction_hash} (status: {tx_status}, gas: {gas_fee} {gas_token})") + logger.info(f"Recorded CLMM REMOVE_LIQUIDITY event: {transaction_hash} " + f"(status: {tx_status}, gas: {gas_fee} {gas_token})") + else: + logger.warning(f"REMOVE_LIQUIDITY {transaction_hash} executed for position " + f"{request.position_address} with no database record — " + "no event recorded (position may be a pending open " + "not yet discovered)") except Exception as db_error: logger.error(f"Error recording REMOVE_LIQUIDITY event: {db_error}", exc_info=True) return { "transaction_hash": transaction_hash, "position_address": request.position_address, - "percentage": float(request.percentage), - "status": "submitted" + "percentage_to_remove": float(request.percentage_to_remove), + "base_token_amount_removed": base_amount_removed, + "quote_token_amount_removed": quote_amount_removed, + "gas_fee": gas_fee, + "status": tx_status.lower() } except HTTPException: @@ -806,7 +794,7 @@ async def remove_liquidity_from_clmm_position( raise HTTPException(status_code=500, detail=f"Error removing liquidity from CLMM position: {str(e)}") -@router.post("/clmm/close", response_model=CLMMCollectFeesResponse) +@router.post("/clmm/close", response_model=CLMMClosePositionResponse) async def close_clmm_position( request: CLMMClosePositionRequest, accounts_service: AccountsService = Depends(get_accounts_service), @@ -831,30 +819,23 @@ async def close_clmm_position( # Parse network_id chain, _ = accounts_service.gateway_client.parse_network_id(request.network) - # Get pool_address and wallet_address from database - pool_address = None - wallet_address = None - + # Wallet resolution: an explicit request value wins (same precedence as + # open/add/remove), then the DB row's wallet, then the default wallet. + db_wallet = None async with db_manager.get_session_context() as session: clmm_repo = GatewayCLMMRepository(session) db_position = await clmm_repo.get_position_by_address(request.position_address) if db_position: - pool_address = db_position.pool_address - wallet_address = db_position.wallet_address - - # If not in database, use default wallet - if not wallet_address: - wallet_address = await accounts_service.gateway_client.get_wallet_address_or_default( - chain=chain, - wallet_address=request.wallet_address - ) + db_wallet = db_position.wallet_address - # If no pool_address from database, we can't query Gateway - if not pool_address: - raise HTTPException( - status_code=404, - detail=f"Position {request.position_address} not found in database. Pool address is required." - ) + wallet_address = request.wallet_address or db_wallet + wallet_address = await accounts_service.gateway_client.get_wallet_address_or_default( + chain=chain, + wallet_address=wallet_address + ) + # Note: neither Gateway's close nor the pre-close snapshot (positions_owned) + # needs the pool — unrecorded positions (e.g. lp_executor opens) close fine + # without one, so pool_address is informational only. # Fetch pending fees and current price BEFORE closing (Gateway doesn't always return these in response) base_fee_to_collect = Decimal("0") @@ -865,8 +846,7 @@ async def close_clmm_position( positions_list = check_gateway_error(await accounts_service.gateway_client.clmm_positions_owned( connector=request.connector, chain_network=request.network, # request.network is already in 'chain-network' format - wallet_address=wallet_address, - pool_address=pool_address + wallet_address=wallet_address )) # Find our specific position and get pending fees and current price @@ -876,7 +856,8 @@ async def close_clmm_position( base_fee_to_collect = Decimal(str(pos.get("baseFeeAmount", 0))) quote_fee_to_collect = Decimal(str(pos.get("quoteFeeAmount", 0))) close_price = float(pos.get("price", 0)) if pos.get("price") else None - logger.info(f"Before closing: price={close_price}, pending fees base={base_fee_to_collect}, quote={quote_fee_to_collect}") + logger.info(f"Before closing: price={close_price}, pending fees " + f"base={base_fee_to_collect}, quote={quote_fee_to_collect}") break else: logger.warning(f"Could not find position {request.position_address} in positions_owned response") @@ -909,9 +890,19 @@ async def close_clmm_position( # Use response values if available, otherwise use pre-fetched values base_fee_collected = Decimal(str(base_fee_from_response)) if base_fee_from_response is not None else base_fee_to_collect - quote_fee_collected = Decimal(str(quote_fee_from_response)) if quote_fee_from_response is not None else quote_fee_to_collect + quote_fee_collected = (Decimal(str(quote_fee_from_response)) + if quote_fee_from_response is not None else quote_fee_to_collect) + + # Confirmed-close accounting: what actually left the pool, and the rent the + # chain refunded for the closed position account (tracked as locked at open + # via position_rent). None until the transaction confirms. + base_amount_removed = data.get("baseTokenAmountRemoved") + quote_amount_removed = data.get("quoteTokenAmountRemoved") + position_rent_refunded = data.get("positionRentRefunded") - logger.info(f"Collected fees on close: base={base_fee_collected}, quote={quote_fee_collected}") + logger.info(f"Collected fees on close: base={base_fee_collected}, quote={quote_fee_collected}; " + f"removed base={base_amount_removed}, quote={quote_amount_removed}; " + f"rent refunded={position_rent_refunded}") # Store CLOSE event in database and update position try: @@ -926,74 +917,98 @@ async def close_clmm_position( "position_id": position.id, "transaction_hash": transaction_hash, "event_type": "CLOSE", - "base_fee_collected": float(base_fee_collected) if base_fee_collected else None, - "quote_fee_collected": float(quote_fee_collected) if quote_fee_collected else None, - "gas_fee": float(gas_fee) if gas_fee else None, + "base_token_amount": float(base_amount_removed) if base_amount_removed is not None else None, + "quote_token_amount": float(quote_amount_removed) if quote_amount_removed is not None else None, + "base_fee_collected": float(base_fee_collected) if base_fee_collected is not None else None, + "quote_fee_collected": float(quote_fee_collected) if quote_fee_collected is not None else None, + "gas_fee": float(gas_fee) if gas_fee is not None else None, "gas_token": gas_token, "status": tx_status } await clmm_repo.create_event(event_data) - logger.info(f"Recorded CLMM CLOSE event: {transaction_hash} (status: {tx_status}, gas: {gas_fee} {gas_token})") - - # Update position: add to collected, reset pending to 0, mark as CLOSED - new_base_collected = Decimal(str(position.base_fee_collected)) + base_fee_collected - new_quote_collected = Decimal(str(position.quote_fee_collected)) + quote_fee_collected - - await clmm_repo.update_position_fees( - position_address=request.position_address, - base_fee_collected=new_base_collected, - quote_fee_collected=new_quote_collected, - base_fee_pending=Decimal("0"), - quote_fee_pending=Decimal("0") - ) - - # Update current_price with close price - if close_price: - await clmm_repo.update_position_liquidity( + logger.info(f"Recorded CLMM CLOSE event: {transaction_hash} " + f"(status: {tx_status}, gas: {gas_fee} {gas_token})") + + # Position bookkeeping happens exactly once, when the tx is known + # good: CONFIRMED here (the event is created CONFIRMED, so the + # poller never touches it), or in the poller's confirm path for + # SUBMITTED events. A FAILED tx mutates nothing — the old + # unconditional booking permanently inflated *_fee_collected on + # failed closes. + if tx_status == "CONFIRMED": + new_base_collected = Decimal(str(position.base_fee_collected)) + base_fee_collected + new_quote_collected = Decimal(str(position.quote_fee_collected)) + quote_fee_collected + + await clmm_repo.update_position_fees( position_address=request.position_address, - base_token_amount=Decimal(str(position.base_token_amount)), - quote_token_amount=Decimal(str(position.quote_token_amount)), - current_price=Decimal(str(close_price)) + base_fee_collected=new_base_collected, + quote_fee_collected=new_quote_collected, + base_fee_pending=Decimal("0"), + quote_fee_pending=Decimal("0") ) - # Verify position is actually closed by checking if it still exists on Gateway - # Gateway returns 500 (or 404) when position doesn't exist - try: - await asyncio.sleep(2) # Wait for transaction to propagate - - verify_result = await accounts_service.gateway_client.clmm_position_info( - connector=request.connector, - chain_network=request.network, - position_address=request.position_address - ) - - # If we get an error response (404 or 500), position is closed - if verify_result and isinstance(verify_result, dict) and "error" in verify_result: - status_code = verify_result.get("status") - if status_code in (404, 500): - await clmm_repo.close_position(request.position_address) - logger.info(f"Position {request.position_address} verified as closed (Gateway returned {status_code})") + # Update current_price with close price + if close_price: + await clmm_repo.update_position_liquidity( + position_address=request.position_address, + base_token_amount=Decimal(str(position.base_token_amount)), + quote_token_amount=Decimal(str(position.quote_token_amount)), + current_price=Decimal(str(close_price)) + ) + + # Verify position is actually gone on Gateway before marking + # CLOSED (some connectors 500 instead of 404 for a + # nonexistent position — right after our own close, either + # means gone). + try: + await asyncio.sleep(2) # Wait for transaction to propagate + + verify_result = await accounts_service.gateway_client.clmm_position_info( + connector=request.connector, + chain_network=request.network, + position_address=request.position_address + ) + + if verify_result and isinstance(verify_result, dict) and "error" in verify_result: + status_code = verify_result.get("status") + if status_code in (404, 500): + await clmm_repo.close_position(request.position_address) + logger.info(f"Position {request.position_address} verified as closed " + f"(Gateway returned {status_code})") + else: + logger.warning(f"Unexpected error verifying position close: {verify_result}") + elif verify_result and "address" in verify_result: + # Position still exists - might be a failed close or delayed propagation + logger.warning(f"Position {request.position_address} still exists after close " + "transaction. Will be handled by poller.") else: - logger.warning(f"Unexpected error verifying position close: {verify_result}") - elif verify_result and "address" in verify_result: - # Position still exists - might be a failed close or delayed propagation - logger.warning(f"Position {request.position_address} still exists after close transaction. Will be handled by poller.") - else: - logger.debug(f"Could not verify position close status, will be handled by poller") - - except Exception as verify_error: - logger.warning(f"Error verifying position close: {verify_error}. Will be handled by poller.") - - logger.info(f"Updated position {request.position_address}: collected fees updated, pending fees reset to 0.") + logger.debug("Could not verify position close status, will be handled by poller") + + except Exception as verify_error: + logger.warning(f"Error verifying position close: {verify_error}. Will be handled by poller.") + + logger.info(f"Updated position {request.position_address}: " + "collected fees updated, pending fees reset to 0.") + else: + # H8 window: a close on a position hapi has no row for (e.g. a + # pending open awaiting the discovery sweep) leaves no event — + # say so loudly instead of silently skipping. + logger.warning(f"CLOSE {transaction_hash} executed for position " + f"{request.position_address} with no database record — " + "no CLOSE event recorded (position may be a pending open " + "not yet discovered)") except Exception as db_error: logger.error(f"Error recording CLOSE event: {db_error}", exc_info=True) - return CLMMCollectFeesResponse( + return CLMMClosePositionResponse( transaction_hash=transaction_hash, position_address=request.position_address, - base_fee_collected=Decimal(str(base_fee_collected)) if base_fee_collected else None, - quote_fee_collected=Decimal(str(quote_fee_collected)) if quote_fee_collected else None, - status="submitted" + base_fee_collected=Decimal(str(base_fee_collected)) if base_fee_collected is not None else None, + quote_fee_collected=Decimal(str(quote_fee_collected)) if quote_fee_collected is not None else None, + base_token_amount_removed=Decimal(str(base_amount_removed)) if base_amount_removed is not None else None, + quote_token_amount_removed=Decimal(str(quote_amount_removed)) if quote_amount_removed is not None else None, + position_rent_refunded=Decimal(str(position_rent_refunded)) if position_rent_refunded is not None else None, + status=tx_status.lower() ) except HTTPException: @@ -1032,30 +1047,23 @@ async def collect_fees_from_clmm_position( # Parse network_id chain, _ = accounts_service.gateway_client.parse_network_id(request.network) - # Get pool_address and wallet_address from database - pool_address = None - wallet_address = None - + # Wallet resolution: an explicit request value wins (same precedence as + # open/add/remove), then the DB row's wallet, then the default wallet. + db_wallet = None async with db_manager.get_session_context() as session: clmm_repo = GatewayCLMMRepository(session) db_position = await clmm_repo.get_position_by_address(request.position_address) if db_position: - pool_address = db_position.pool_address - wallet_address = db_position.wallet_address - - # If not in database, use default wallet - if not wallet_address: - wallet_address = await accounts_service.gateway_client.get_wallet_address_or_default( - chain=chain, - wallet_address=request.wallet_address - ) + db_wallet = db_position.wallet_address - # If no pool_address from database, we can't query Gateway - if not pool_address: - raise HTTPException( - status_code=404, - detail=f"Position {request.position_address} not found in database. Pool address is required." - ) + wallet_address = request.wallet_address or db_wallet + wallet_address = await accounts_service.gateway_client.get_wallet_address_or_default( + chain=chain, + wallet_address=wallet_address + ) + # Note: neither Gateway's collect nor the fee snapshot (positions_owned) needs + # the pool — unrecorded positions work without one; pool_address is + # informational only. # Fetch pending fees BEFORE collecting (Gateway doesn't always return collected amounts in response) base_fee_to_collect = Decimal("0") @@ -1065,8 +1073,7 @@ async def collect_fees_from_clmm_position( positions_list = check_gateway_error(await accounts_service.gateway_client.clmm_positions_owned( connector=request.connector, chain_network=request.network, # request.network is already in 'chain-network' format - wallet_address=wallet_address, - pool_address=pool_address + wallet_address=wallet_address )) # Find our specific position and get pending fees @@ -1104,7 +1111,8 @@ async def collect_fees_from_clmm_position( # Use response values if available, otherwise use pre-fetched values base_fee_collected = Decimal(str(base_fee_from_response)) if base_fee_from_response is not None else base_fee_to_collect - quote_fee_collected = Decimal(str(quote_fee_from_response)) if quote_fee_from_response is not None else quote_fee_to_collect + quote_fee_collected = (Decimal(str(quote_fee_from_response)) + if quote_fee_from_response is not None else quote_fee_to_collect) # Extract gas fee from Gateway response gas_fee = data.get("fee") @@ -1125,36 +1133,47 @@ async def collect_fees_from_clmm_position( "position_id": position.id, "transaction_hash": transaction_hash, "event_type": "COLLECT_FEES", - "base_fee_collected": float(base_fee_collected) if base_fee_collected else None, - "quote_fee_collected": float(quote_fee_collected) if quote_fee_collected else None, - "gas_fee": float(gas_fee) if gas_fee else None, + "base_fee_collected": float(base_fee_collected) if base_fee_collected is not None else None, + "quote_fee_collected": float(quote_fee_collected) if quote_fee_collected is not None else None, + "gas_fee": float(gas_fee) if gas_fee is not None else None, "gas_token": gas_token, "status": tx_status } await clmm_repo.create_event(event_data) - logger.info(f"Recorded CLMM COLLECT_FEES event: {transaction_hash} (status: {tx_status}, gas: {gas_fee} {gas_token})") - - # Update position: add to collected, reset pending to 0 - new_base_collected = Decimal(str(position.base_fee_collected)) + base_fee_collected - new_quote_collected = Decimal(str(position.quote_fee_collected)) + quote_fee_collected - - await clmm_repo.update_position_fees( - position_address=request.position_address, - base_fee_collected=new_base_collected, - quote_fee_collected=new_quote_collected, - base_fee_pending=Decimal("0"), - quote_fee_pending=Decimal("0") - ) - logger.info(f"Updated position {request.position_address}: collected fees updated, pending fees reset to 0") + logger.info(f"Recorded CLMM COLLECT_FEES event: {transaction_hash} " + f"(status: {tx_status}, gas: {gas_fee} {gas_token})") + + # Book fees exactly once: CONFIRMED here (event created CONFIRMED, + # never re-processed), SUBMITTED in the poller's confirm path. + # The old unconditional booking double-counted every pending + # collect (endpoint + poller) and kept phantom fees on failures. + if tx_status == "CONFIRMED": + new_base_collected = Decimal(str(position.base_fee_collected)) + base_fee_collected + new_quote_collected = Decimal(str(position.quote_fee_collected)) + quote_fee_collected + + await clmm_repo.update_position_fees( + position_address=request.position_address, + base_fee_collected=new_base_collected, + quote_fee_collected=new_quote_collected, + base_fee_pending=Decimal("0"), + quote_fee_pending=Decimal("0") + ) + logger.info(f"Updated position {request.position_address}: " + "collected fees updated, pending fees reset to 0") + else: + logger.warning(f"COLLECT_FEES {transaction_hash} executed for position " + f"{request.position_address} with no database record — " + "no event recorded (position may be a pending open " + "not yet discovered)") except Exception as db_error: logger.error(f"Error recording COLLECT_FEES event: {db_error}", exc_info=True) return CLMMCollectFeesResponse( transaction_hash=transaction_hash, position_address=request.position_address, - base_fee_collected=Decimal(str(base_fee_collected)) if base_fee_collected else None, - quote_fee_collected=Decimal(str(quote_fee_collected)) if quote_fee_collected else None, - status="submitted" + base_fee_collected=Decimal(str(base_fee_collected)) if base_fee_collected is not None else None, + quote_fee_collected=Decimal(str(quote_fee_collected)) if quote_fee_collected is not None else None, + status=tx_status.lower() ) except HTTPException: @@ -1174,16 +1193,20 @@ async def get_clmm_positions_owned( accounts_service: AccountsService = Depends(get_accounts_service) ): """ - Get all CLMM liquidity positions owned by a wallet for a specific pool. + Get all CLMM liquidity positions owned by a wallet. + + Mirrors Gateway's /trading/clmm/positions-owned, which takes no pool filter: + every CLMM position the wallet owns on the connector is returned, each row + carrying its own pool_address. (The old pool_address request field was a + silent no-op — Gateway never read it and the response was never filtered.) Example: connector: 'meteora' network: 'solana-mainnet-beta' - pool_address: '2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3' wallet_address: (optional, uses default if not provided) Returns: - List of CLMM position information for the specified pool + List of CLMM position information """ try: if not await accounts_service.gateway_client.ping(): @@ -1198,12 +1221,10 @@ async def get_clmm_positions_owned( wallet_address=request.wallet_address ) - # Get positions for the specified pool result = check_gateway_error(await accounts_service.gateway_client.clmm_positions_owned( connector=request.connector, chain_network=request.network, # request.network is already in 'chain-network' format - wallet_address=wallet_address, - pool_address=request.pool_address + wallet_address=wallet_address )) # Gateway returns a list directly @@ -1260,6 +1281,160 @@ async def get_clmm_positions_owned( raise HTTPException(status_code=500, detail=f"Error getting CLMM positions owned: {str(e)}") +@router.post("/clmm/quote-position", response_model=CLMMQuotePositionResponse, response_model_by_alias=False) +async def quote_clmm_position( + request: CLMMQuotePositionRequest, + accounts_service: AccountsService = Depends(get_accounts_service) +): + """ + Quote a candidate CLMM position before opening or adding liquidity. + + Mirrors Gateway's GET /trading/clmm/quote-position: returns the base/quote + split the pool would actually take for the given range and deposit amounts + (and which side limits it), without signing or submitting anything. + """ + try: + if not await accounts_service.gateway_client.ping(): + raise HTTPException(status_code=503, detail="Gateway service is not available") + + result = check_gateway_error(await accounts_service.gateway_client.clmm_quote_position( + connector=request.connector, + chain_network=request.network, + pool_address=request.pool_address, + lower_price=float(request.lower_price), + upper_price=float(request.upper_price), + base_token_amount=float(request.base_token_amount) if request.base_token_amount is not None else None, + quote_token_amount=float(request.quote_token_amount) if request.quote_token_amount is not None else None, + slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else None, + )) + return CLMMQuotePositionResponse(**result) + + except HTTPException: + raise + except GatewayError as e: + raise HTTPException(status_code=e.status, detail=f"Gateway error quoting CLMM position: {e}") + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.error(f"Error quoting CLMM position: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Error quoting CLMM position: {str(e)}") + + +@router.post("/clmm/create-pool", response_model=AMMCreatePoolResponse, response_model_by_alias=False) +async def create_clmm_pool( + request: CLMMCreatePoolRequest, + accounts_service: AccountsService = Depends(get_accounts_service) +): + """ + Create a new (empty) CLMM pool — liquidity is added afterwards by opening positions. + + Mirrors Gateway's POST /trading/clmm/create-pool (which shares the AMM + create-pool response shape). Connector-specific params ride extra_params + under Gateway's own names — the same contract as open's extra_params. + """ + try: + validate_extra_params(request.extra_params, CLMM_CREATE_POOL_EXTRA_PARAMS_SPEC, + request.connector, "unified /trading/clmm/create-pool") + + if not await accounts_service.gateway_client.ping(): + raise HTTPException(status_code=503, detail="Gateway service is not available") + + chain, _ = accounts_service.gateway_client.parse_network_id(request.network) + wallet_address = await accounts_service.gateway_client.get_wallet_address_or_default( + chain=chain, + wallet_address=request.wallet_address + ) + + result = check_gateway_error(await accounts_service.gateway_client.clmm_create_pool( + connector=request.connector, + chain_network=request.network, + wallet_address=wallet_address, + base_token=request.base_token, + quote_token=request.quote_token, + initial_price=float(request.initial_price) if request.initial_price is not None else None, + extra_params=request.extra_params, + )) + return AMMCreatePoolResponse(**result) + + except HTTPException: + raise + except GatewayError as e: + raise HTTPException(status_code=e.status, detail=f"Gateway error creating CLMM pool: {e}") + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + logger.error(f"Error creating CLMM pool: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Error creating CLMM pool: {str(e)}") + + +@router.get("/clmm/position-info", response_model=CLMMPositionInfo) +async def get_clmm_position_info( + connector: str, + network: str, + position_address: str, + accounts_service: AccountsService = Depends(get_accounts_service) +): + """ + Get a single CLMM position by its address. + + Mirrors Gateway's GET /trading/clmm/position-info. Gateway reports a missing + or closed position as an error (500/404), surfaced here as 404. + """ + try: + if not await accounts_service.gateway_client.ping(): + raise HTTPException(status_code=503, detail="Gateway service is not available") + + pos = await accounts_service.gateway_client.clmm_position_info( + connector=connector, + chain_network=network, + position_address=position_address + ) + if pos is None or not isinstance(pos, dict): + # Connection error: the client returns None — a 503, not a crash. + raise HTTPException(status_code=503, detail="Gateway service is not available") + if "error" in pos: + status_code = pos.get("status") + if status_code in (404, 500): + raise HTTPException(status_code=404, detail=f"Position {position_address} not found or closed") + raise HTTPException(status_code=status_code or 502, detail=str(pos.get("error"))) + + base_token_address = pos.get("baseTokenAddress", "") + quote_token_address = pos.get("quoteTokenAddress", "") + base_token = base_token_address[-8:] if base_token_address else "" + quote_token = quote_token_address[-8:] if quote_token_address else "" + current_price = Decimal(str(pos.get("price", 0))) + lower_price = Decimal(str(pos.get("lowerPrice", 0))) if pos.get("lowerPrice") else Decimal("0") + upper_price = Decimal(str(pos.get("upperPrice", 0))) if pos.get("upperPrice") else Decimal("0") + in_range = bool(current_price > 0 and lower_price > 0 and upper_price > 0 + and lower_price <= current_price <= upper_price) + + return CLMMPositionInfo( + position_address=pos.get("address", position_address), + pool_address=pos.get("poolAddress", ""), + trading_pair=f"{base_token}-{quote_token}" if base_token and quote_token else "", + base_token=base_token, + quote_token=quote_token, + base_token_amount=Decimal(str(pos.get("baseTokenAmount", 0))), + quote_token_amount=Decimal(str(pos.get("quoteTokenAmount", 0))), + current_price=current_price, + lower_price=lower_price, + upper_price=upper_price, + base_fee_amount=Decimal(str(pos["baseFeeAmount"])) if pos.get("baseFeeAmount") is not None else None, + quote_fee_amount=Decimal(str(pos["quoteFeeAmount"])) if pos.get("quoteFeeAmount") is not None else None, + lower_bin_id=pos.get("lowerBinId"), + upper_bin_id=pos.get("upperBinId"), + in_range=in_range + ) + + except HTTPException: + raise + except GatewayError as e: + raise HTTPException(status_code=e.status, detail=f"Gateway error getting CLMM position: {e}") + except Exception as e: + logger.error(f"Error getting CLMM position info: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"Error getting CLMM position info: {str(e)}") + + @router.get("/clmm/positions/{position_address}/events") async def get_clmm_position_events( position_address: str, @@ -1272,7 +1447,9 @@ async def get_clmm_position_events( Args: position_address: Position NFT address - event_type: Filter by event type (OPEN, ADD_LIQUIDITY, REMOVE_LIQUIDITY, COLLECT_FEES, CLOSE) + event_type: Filter by event type (OPEN, ADD_LIQUIDITY, REMOVE_LIQUIDITY, COLLECT_FEES, CLOSE, + DISCOVERED — written by the poller for positions it found on-chain, with a + synthetic discovered_ transaction hash) limit: Max events to return Returns: @@ -1318,7 +1495,7 @@ async def search_clmm_positions( network: Filter by network (e.g., 'solana-mainnet-beta') connector: Filter by connector (e.g., 'meteora') wallet_address: Filter by wallet address - trading_pair: Filter by trading pair (e.g., 'SOL-USDC') + trading_pair: Filter by trading pair (address-derived identifiers as stored; symbol pairs like 'SOL-USDC' will not match) status: Filter by status (OPEN, CLOSED) position_addresses: Filter by specific position addresses (list of addresses) limit: Max results (default 50, max 1000) @@ -1405,5 +1582,3 @@ async def search_clmm_positions( except Exception as e: logger.error(f"Error searching CLMM positions: {e}", exc_info=True) raise HTTPException(status_code=500, detail=f"Error searching CLMM positions: {str(e)}") - - diff --git a/routers/gateway_extras.py b/routers/gateway_extras.py new file mode 100644 index 00000000..f8ac83de --- /dev/null +++ b/routers/gateway_extras.py @@ -0,0 +1,78 @@ +""" +Shared helpers for the gateway trading routers: extra_params validation and +Gateway write-response status mapping. + +Gateway destructures a fixed set of connector-specific keys from each request +body and silently ignores everything else, so hapi rejects loudly instead of +letting a typo'd key, a key sent to a connector that ignores it, or a value of +the wrong type get silently dropped or misparsed downstream. +""" +from typing import Any, Dict, Optional, Set, Tuple + +from fastapi import HTTPException + + +def get_transaction_status_from_response(gateway_response: dict) -> str: + """Map a Gateway write response's status to hapi's transaction vocabulary: + status 1 -> CONFIRMED, negative (-1 failed, -2 dropped) -> FAILED, + 0 or missing -> SUBMITTED. + + Single source: both the swap and CLMM routers import this — two drifting + copies of load-bearing status mapping is how a failed tx gets recorded as + submitted on one surface but not the other. + """ + status = gateway_response.get("status") + + if status == 1: + return "CONFIRMED" + # Gateway's TransactionStatus uses negative values for terminal failures + # (e.g. a failed EVM swap returns status -1 with zeroed amounts). + if isinstance(status, (int, float)) and status < 0: + return "FAILED" + return "SUBMITTED" + + +# Spec entry: key -> (allowed value types, connectors that honor the key). +# Deliberate strictness (accepted residual): numeric keys are typed int even though +# Gateway's TypeBox says Number (JS has one number type) — the domains are integral +# (enum values, bin steps, config indexes), so a float like 1.0 gets a loud 400 +# here rather than reaching Gateway; no semantically distinct value is blocked. +ExtraParamsSpec = Dict[str, Tuple[Tuple[type, ...], Set[str]]] + + +def validate_extra_params( + extra_params: Optional[Dict[str, Any]], + spec: ExtraParamsSpec, + connector: str, + route: str, +) -> None: + """Reject extra_params Gateway's `route` would silently drop or misparse. + + Typed swap providers like 'jupiter/router' validate on their base name. + """ + for key, value in (extra_params or {}).items(): + if key not in spec: + raise HTTPException( + status_code=400, + detail=( + f"Unsupported extra_params key '{key}': Gateway's {route} honors " + f"only {sorted(spec)} and silently ignores everything else." + ), + ) + allowed_types, connectors = spec[key] + if connector.split("/")[0] not in connectors: + raise HTTPException( + status_code=400, + detail=( + f"extra_params.{key} only applies to {sorted(connectors)}; " + f"'{connector}' would silently ignore it." + ), + ) + # bool subclasses int: check it explicitly so True never passes as an int. + type_ok = (bool in allowed_types) if isinstance(value, bool) else isinstance(value, allowed_types) + if not type_ok: + expected = "/".join(t.__name__ for t in allowed_types) + raise HTTPException( + status_code=400, + detail=f"extra_params.{key} must be {expected}, got {type(value).__name__} ({value!r}).", + ) diff --git a/routers/gateway_swap.py b/routers/gateway_swap.py index 3a5816c6..17381bd8 100644 --- a/routers/gateway_swap.py +++ b/routers/gateway_swap.py @@ -14,6 +14,7 @@ from database.repositories import GatewaySwapRepository from deps import get_accounts_service, get_database_manager from models import SwapExecuteRequest, SwapExecuteResponse, SwapQuoteRequest, SwapQuoteResponse +from routers.gateway_extras import ExtraParamsSpec, get_transaction_status_from_response, validate_extra_params from services.accounts_service import AccountsService from services.gateway_client import GatewayError, check_gateway_error @@ -22,26 +23,11 @@ router = APIRouter(tags=["Gateway Swaps"], prefix="/gateway") -def get_transaction_status_from_response(gateway_response: dict) -> str: - """ - Determine transaction status from Gateway response. - - Gateway returns status field in the response: - - status: 1 = confirmed - - status: 0 = pending/submitted - - Returns: - "CONFIRMED" if status == 1 - "SUBMITTED" if status == 0 or not present - """ - status = gateway_response.get("status") - - # Status 1 means transaction is confirmed on-chain - if status == 1: - return "CONFIRMED" - - # Status 0 or missing means submitted but not confirmed yet - return "SUBMITTED" +# Gateway's unified /trading/swap routes pass approximateIfNoExactOut only to the +# Solana router connectors' quote path; every other provider silently ignores it. +SWAP_EXTRA_PARAMS_SPEC: ExtraParamsSpec = { + "approximateIfNoExactOut": ((bool,), {"jupiter", "dflow", "okx", "titan"}), +} @router.post("/swap/quote", response_model=SwapQuoteResponse) @@ -58,12 +44,16 @@ async def get_swap_quote( trading_pair: 'SOL-USDC' side: 'BUY' amount: 1 - slippage_pct: 1 + slippage_pct: 1 (optional; omit to use the connector's configured slippagePct) + extra_params: {"approximateIfNoExactOut": false} # Solana routers Returns: - Quote with price, expected output amount, and gas estimate + Quote with price, expected output amount, and execution-safety fields """ try: + validate_extra_params(request.extra_params, SWAP_EXTRA_PARAMS_SPEC, + request.connector, "unified /trading/swap/quote") + if not await accounts_service.gateway_client.ping(): raise HTTPException(status_code=503, detail="Gateway service is not available") @@ -78,30 +68,30 @@ async def get_swap_quote( quote_asset=quote, amount=float(request.amount), side=request.side, - slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else 1.0 + slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else None, + extra_params=request.extra_params )) - # Extract amounts from Gateway response (snake_case for consistency) - amount_in_raw = result.get("amountIn") or result.get("amount_in") - amount_out_raw = result.get("amountOut") or result.get("amount_out") - - amount_in = Decimal(str(amount_in_raw)) if amount_in_raw else None - amount_out = Decimal(str(amount_out_raw)) if amount_out_raw else None - - # Extract gas estimate (try both camelCase and snake_case) - gas_estimate = result.get("gasEstimate") or result.get("gas_estimate") - gas_estimate_value = Decimal(str(gas_estimate)) if gas_estimate else None + # Re-frame Gateway's token-flow response (tokenIn/tokenOut) into pair terms, + # passing its execution-safety fields through in snake_case. + def _dec(key): + value = result.get(key) + return Decimal(str(value)) if value is not None else None return SwapQuoteResponse( base=base, quote=quote, price=Decimal(str(result.get("price", 0))), amount=request.amount, - amount_in=amount_in, - amount_out=amount_out, - expected_amount=amount_out, # Deprecated, kept for backward compatibility - slippage_pct=request.slippage_pct if request.slippage_pct is not None else Decimal("1.0"), - gas_estimate=gas_estimate_value + amount_in=_dec("amountIn"), + amount_out=_dec("amountOut"), + min_amount_out=_dec("minAmountOut"), + max_amount_in=_dec("maxAmountIn"), + price_impact_pct=_dec("priceImpactPct"), + pool_address=result.get("poolAddress"), + route_path=result.get("routePath"), + slippage_pct=(_dec("slippagePct") if result.get("slippagePct") is not None + else request.slippage_pct), ) except HTTPException: @@ -130,13 +120,17 @@ async def execute_swap( trading_pair: 'SOL-USDC' side: 'BUY' amount: 1 - slippage_pct: 1 + slippage_pct: 1 (optional; omit to use the connector's configured slippagePct) wallet_address: (optional, uses default if not provided) + extra_params: {"approximateIfNoExactOut": false} # Solana routers Returns: Transaction hash and swap details """ try: + validate_extra_params(request.extra_params, SWAP_EXTRA_PARAMS_SPEC, + request.connector, "unified /trading/swap/execute") + if not await accounts_service.gateway_client.ping(): raise HTTPException(status_code=503, detail="Gateway service is not available") @@ -161,25 +155,37 @@ async def execute_swap( quote_asset=quote, amount=float(request.amount), side=request.side, - slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else 1.0 + slippage_pct=float(request.slippage_pct) if request.slippage_pct is not None else None, + extra_params=request.extra_params )) transaction_hash = result.get("signature") or result.get("txHash") or result.get("hash") if not transaction_hash: raise HTTPException(status_code=500, detail="No transaction hash returned from Gateway") - # Extract swap data from Gateway response - # Gateway returns amounts nested under 'data' object + # Gateway's `data` (present only when it confirmed the tx) speaks token flow: + # amountIn is the tokenIn amount — quote for BUY, base for SELL — and the DB + # columns keep that tokenIn/tokenOut denomination. data = result.get("data", {}) amount_in_raw = data.get("amountIn") amount_out_raw = data.get("amountOut") - # Use amounts from Gateway response, fallback to request amount if not available - input_amount = Decimal(str(amount_in_raw)) if amount_in_raw is not None else request.amount - output_amount = Decimal(str(amount_out_raw)) if amount_out_raw is not None else Decimal("0") - - # Calculate price from actual swap amounts - # Price = output / input (how much quote you get/pay per base) - price = output_amount / input_amount if input_amount > 0 else Decimal("0") + side = request.side.upper() + if amount_in_raw is not None and amount_out_raw is not None: + input_amount = Decimal(str(amount_in_raw)) + output_amount = Decimal(str(amount_out_raw)) + # Price in quote-per-base for both sides: SELL flows base->quote (out/in), + # BUY flows quote->base (in/out). + if side == "SELL": + price = output_amount / input_amount if input_amount > 0 else Decimal("0") + else: + price = input_amount / output_amount if output_amount > 0 else Decimal("0") + else: + # Submitted-not-confirmed: only the request leg of the flow is known — + # request.amount is base-denominated (SELL: base in, BUY: base out). + # The unknown leg and price stay 0 placeholders. + input_amount = request.amount if side == "SELL" else Decimal("0") + output_amount = request.amount if side == "BUY" else Decimal("0") + price = Decimal("0") # Get transaction status from Gateway response tx_status = get_transaction_status_from_response(result) @@ -192,18 +198,21 @@ async def execute_swap( swap_data = { "transaction_hash": transaction_hash, "network": request.network, - "connector": request.connector, + # Store the base venue name: a swap on "jupiter" and one on + # "jupiter/router" are the same venue and must file together. + "connector": request.connector.split("/")[0], "wallet_address": wallet_address, "trading_pair": request.trading_pair, "base_token": base, "quote_token": quote, - "side": request.side, + "side": side, "input_amount": float(input_amount), "output_amount": float(output_amount), "price": float(price), - "slippage_pct": float(request.slippage_pct) if request.slippage_pct is not None else 1.0, + "slippage_pct": float(request.slippage_pct) if request.slippage_pct is not None else None, "status": tx_status, - "pool_address": result.get("poolAddress") or result.get("pool_address") + # Gateway's execute response schema carries no pool information + "pool_address": None } await swap_repo.create_swap(swap_data) @@ -215,9 +224,11 @@ async def execute_swap( return SwapExecuteResponse( transaction_hash=transaction_hash, trading_pair=request.trading_pair, - side=request.side, + side=side, amount=request.amount, - status="submitted" + # "confirmed" / "submitted" / "failed" — a failed EVM swap comes back as + # status -1 with zeroed amounts, which must not read as in-flight. + status=tx_status ) except HTTPException: diff --git a/services/executor_service.py b/services/executor_service.py index 21c235c1..46e58143 100644 --- a/services/executor_service.py +++ b/services/executor_service.py @@ -629,6 +629,31 @@ async def stop_executor( """ executor = self._active_executors.get(executor_id) if not executor: + # Terminal executors are popped from memory within one control-loop tick, + # so "not in memory" usually means "already terminated", not "unknown". + # Fall back to the DB and answer with the final state as a no-op success. + # This deliberately includes rows still marked RUNNING in the DB: an + # executor known to the DB but absent from memory is dead regardless of + # its stored status (completion race, failed persist, restart window), + # and answering 404 there is the gateway#678 dead end. 404 is reserved + # for executor ids the DB has never seen (or a DB outage, which + # get_executor logs and swallows to None). + db_record = await self.get_executor(executor_id) + if db_record: + custom_info = db_record.get("custom_info") or {} + logger.info( + f"Stop requested for already-terminated executor {executor_id} " + f"(db status: {db_record.get('status')}, close_type: {db_record.get('close_type')}) - no-op" + ) + return { + "executor_id": executor_id, + "status": "already_terminated", + "keep_position": keep_position, + "close_type": db_record.get("close_type"), + "position_address": custom_info.get("position_address"), + "orphaned_position": bool(custom_info.get("orphaned_position", False)), + "hold_reason": custom_info.get("hold_reason"), + } raise HTTPException(status_code=404, detail=f"Executor {executor_id} not found") if executor.is_closed: @@ -649,6 +674,129 @@ async def stop_executor( "keep_position": keep_position } + async def get_orphaned_positions(self) -> List[Dict[str, Any]]: + """ + List executors that terminated while potentially still owning an on-chain position. + + Covers both orphan classes: + - close_type FAILED with a position_address in the persisted final state + (e.g. an LP close that exhausted retries - gateway#678) + - close_type SYSTEM_CLEANUP (RUNNING rows rewritten after an API restart); + these have no final state, so the position address is unknown and the + on-chain state must be reconciled externally + + This listing is DB-side only: cross-check candidates against on-chain reality + (gateway CLMM/AMM positions-owned endpoints) before recovering. Recovered + orphans are silenced with resolve_orphaned_position(). + + Raises on DB errors rather than returning [] - "no orphans" from a broken DB + would read as all-clear on a safety endpoint. + """ + if not self.db_manager: + raise RuntimeError("Orphan listing unavailable: no database configured") + + # lp_executor is the only executor type that owns an on-chain position + # account; filtering in SQL keeps the limit meaningful (a Python-side filter + # over the newest N mixed candidates can silently drop older real orphans). + async with self.db_manager.get_session_context() as session: + repo = ExecutorRepository(session) + records = await repo.get_executors_by_close_types( + ["FAILED", "SYSTEM_CLEANUP", "POSITION_HOLD"], executor_type="lp_executor" + ) + + orphans: List[Dict[str, Any]] = [] + for record in records: + final_state: Dict[str, Any] = {} + if record.final_state: + try: + final_state = json.loads(record.final_state) + except (json.JSONDecodeError, TypeError): + final_state = {} + + if final_state.get("orphan_resolved"): + continue + + position_address = final_state.get("position_address") + if record.close_type == "FAILED" and not position_address: + # Failed without on-chain exposure - not an orphan + continue + if record.close_type == "POSITION_HOLD" and not ( + final_state.get("orphaned_position") or final_state.get("hold_reason") + ): + # Voluntary hold (keep_position=True stop) - position was closed on-chain + continue + + # The DEX and pool live in the executor config, not in any column: for lp_executor the + # connector_name column carries the network id. Both are needed to close the position, + # and LP-executor positions are opened by the bot straight against Gateway so they are + # never in the API's own CLMM position table to be looked up there. + config: Dict[str, Any] = {} + if record.config: + try: + config = json.loads(record.config) + except (json.JSONDecodeError, TypeError): + config = {} + + orphans.append({ + "executor_id": record.executor_id, + "executor_type": record.executor_type, + "account_name": record.account_name, + "connector_name": record.connector_name, + "trading_pair": record.trading_pair, + "lp_provider": config.get("lp_provider"), + "pool_address": config.get("pool_address"), + "controller_id": record.controller_id or "main", + "close_type": record.close_type, + "closed_at": record.closed_at.isoformat() if record.closed_at else None, + "position_address": position_address, + "state": final_state.get("state"), + "hold_reason": final_state.get("hold_reason"), + "needs_onchain_reconciliation": position_address is None, + }) + + return orphans + + async def resolve_orphaned_position(self, executor_id: str) -> Dict[str, Any]: + """ + Mark an orphaned position as recovered so it stops surfacing. + + Call this after the stranded on-chain position has been closed (or adopted) + externally. Sets orphan_resolved in the persisted final state, which removes + the record from get_orphaned_positions() and from agent-facing warnings. + """ + if not self.db_manager: + raise HTTPException(status_code=503, detail="No database configured") + + async with self.db_manager.get_session_context() as session: + repo = ExecutorRepository(session) + record = await repo.get_executor_by_id(executor_id) + if not record: + raise HTTPException(status_code=404, detail=f"Executor {executor_id} not found") + if record.status == "RUNNING": + raise HTTPException(status_code=400, detail=f"Executor {executor_id} is still running") + + final_state: Dict[str, Any] = {} + if record.final_state: + try: + final_state = json.loads(record.final_state) + except (json.JSONDecodeError, TypeError): + final_state = {} + + is_involuntary_hold = record.close_type == "POSITION_HOLD" and ( + final_state.get("orphaned_position") or final_state.get("hold_reason") + ) + if record.close_type not in ("FAILED", "SYSTEM_CLEANUP") and not is_involuntary_hold: + raise HTTPException( + status_code=400, + detail=f"Executor {executor_id} (close_type: {record.close_type}) is not an orphan candidate", + ) + final_state["orphaned_position"] = False + final_state["orphan_resolved"] = True + await repo.update_executor(executor_id=executor_id, final_state=json.dumps(final_state)) + + logger.info(f"Orphaned position for executor {executor_id} marked resolved") + return {"executor_id": executor_id, "orphan_resolved": True} + async def _handle_executor_completion(self, executor_id: str): """Handle cleanup when an executor completes.""" # Atomically claim the executor so a concurrent completion (e.g. the @@ -679,6 +827,24 @@ async def _handle_executor_completion(self, executor_id: str): close_type = executor.close_type.name if executor.close_type else "UNKNOWN" logger.info(f"Executor {executor_id} completed with close_type: {close_type}") + # Surface stranded on-chain exposure loudly: a live position address on an + # involuntary hold (hold_reason set) or a legacy FAILED means the position + # has no automated owner from this point on. + if executor.close_type in (CloseType.FAILED, CloseType.POSITION_HOLD): + try: + completion_info = executor.get_custom_info() + position_address = completion_info.get("position_address") + hold_reason = completion_info.get("hold_reason") + except Exception: + position_address = None + hold_reason = None + if position_address and (hold_reason or executor.close_type == CloseType.FAILED): + logger.error( + f"Executor {executor_id} ended {close_type} with position {position_address} " + f"still open on-chain (hold_reason: {hold_reason}) - orphaned position requires " + "recovery (flagged in DB record; see /executors/positions/orphaned)" + ) + def _format_executor_info( self, executor_id: str, @@ -996,6 +1162,17 @@ async def _persist_executor_completed(self, executor_id: str, executor: Executor # Get custom_info directly from executor to avoid Pydantic serialization issues # with TrackedOrder and other complex types custom_info = executor.get_custom_info() + + # A stranded live on-chain position: an involuntary hold (close retries + # exhausted -> POSITION_HOLD with hold_reason set, gateway#678) or a legacy + # FAILED-with-position (force-stop straggler, older wheel). Flag it in the + # persisted final_state so /executors/positions/orphaned, dashboards, and + # agents can find and recover it. Voluntary holds never match: a successful + # close clears position_address before the executor terminates. + if custom_info.get("position_address") and ( + custom_info.get("hold_reason") or close_type == "FAILED" + ): + custom_info["orphaned_position"] = True # Serialize custom_info, fallback to None if serialization fails final_state_json = None metadata = self._executor_metadata.get(executor_id, {}) diff --git a/services/gateway_client.py b/services/gateway_client.py index cc15d8d1..037e60fb 100644 --- a/services/gateway_client.py +++ b/services/gateway_client.py @@ -6,17 +6,43 @@ logger = logging.getLogger(__name__) -# Connectors whose bare name maps to a router-type swap provider on Gateway. -# All other connectors default to their CLMM route (meteora, orca, raydium, pancakeswap-sol). -ROUTER_CONNECTORS = {"jupiter", "0x", "uniswap", "pancakeswap"} +# When a caller names a connector without a trading type, Gateway's own +# config/connectors listing decides the type — preferring a router route, then +# CLMM, then AMM. A hardcoded roster silently misrouted every connector Gateway +# added after it was written. +_SWAP_TYPE_PREFERENCE = ("router", "clmm", "amm") + +# The single source for chain -> native gas token. Every writer of gas_token +# columns (routers and the transaction poller) must use this — drifted local +# copies previously produced "MATIC", None, and "UNKNOWN" for the same chain. +_NATIVE_GAS_TOKENS = { + "solana": "SOL", + "ethereum": "ETH", + "polygon": "MATIC", + "avalanche": "AVAX", + "optimism": "ETH", + "arbitrum": "ETH", + "base": "ETH", + "bsc": "BNB", + "cronos": "CRO", +} + + +def get_native_gas_token(chain: str) -> str: + """Native gas token symbol for a chain (e.g. 'solana' -> 'SOL').""" + return _NATIVE_GAS_TOKENS.get(chain.lower(), "UNKNOWN") class GatewayError(Exception): """A Gateway HTTP request that completed with a non-OK status.""" - def __init__(self, message: str, status: int): + def __init__(self, message: str, status: int, code: Optional[str] = None): super().__init__(message) self.status = status + # Gateway's machine-readable error code (TRANSACTION_TIMEOUT, + # SLIPPAGE_EXCEEDED, ...). Callers branch on this instead of + # string-matching the message. + self.code = code def check_gateway_error(result: Optional[Any]) -> Any: @@ -30,8 +56,8 @@ def check_gateway_error(result: Optional[Any]) -> Any: """ if result is None: raise GatewayError("No response from Gateway (connection error)", 503) - if isinstance(result, dict) and set(result.keys()) == {"error", "status"}: - raise GatewayError(str(result["error"]), int(result["status"])) + if isinstance(result, dict) and {"error", "status"} <= set(result.keys()) <= {"error", "status", "code"}: + raise GatewayError(str(result["error"]), int(result["status"]), result.get("code")) return result @@ -64,6 +90,8 @@ def __init__( # while the Gateway is simply not started. Logged once on the transition, then suppressed # until certs become available again. self._certs_unavailable_warned = False + # Gateway's connector -> trading_types listing, fetched on first use. + self._connector_trading_types: Optional[Dict[str, List[str]]] = None @staticmethod def parse_network_id(network_id: str) -> tuple[str, str]: @@ -87,8 +115,10 @@ async def get_wallet_address_or_default(self, chain: str, wallet_address: Option default_wallet = await self.get_default_wallet_address(chain) if not default_wallet: raise ValueError(f"No wallet configured for chain '{chain}'") - # Skip placeholder wallet addresses (e.g., "ethereum-default-wallet", "solana-default-wallet") - if default_wallet.endswith("-default-wallet"): + # Gateway's fresh config templates write "" / + # "" as the placeholder — passing that literal + # string on would surface as an opaque Gateway address-validation error. + if default_wallet.startswith("<") and default_wallet.endswith(">"): raise ValueError(f"No valid wallet configured for chain '{chain}' (found placeholder: {default_wallet})") return default_wallet @@ -144,23 +174,23 @@ async def _request(self, method: str, path: str, params: Dict = None, json: Dict if method == "GET": async with session.get(url, params=params) as response: if not response.ok: - error_body = await self._get_error_body(response) + error_body, error_code = await self._get_error_body(response) logger.warning(f"Gateway request failed: {method} {url} - {response.status} - {error_body}") - return {"error": error_body, "status": response.status} + return {"error": error_body, "status": response.status, "code": error_code} return await response.json() elif method == "POST": async with session.post(url, params=params, json=json) as response: if not response.ok: - error_body = await self._get_error_body(response) + error_body, error_code = await self._get_error_body(response) logger.warning(f"Gateway request failed: {method} {url} - {response.status} - {error_body}") - return {"error": error_body, "status": response.status} + return {"error": error_body, "status": response.status, "code": error_code} return await response.json() elif method == "DELETE": async with session.delete(url, params=params, json=json) as response: if not response.ok: - error_body = await self._get_error_body(response) + error_body, error_code = await self._get_error_body(response) logger.warning(f"Gateway request failed: {method} {url} - {response.status} - {error_body}") - return {"error": error_body, "status": response.status} + return {"error": error_body, "status": response.status, "code": error_code} return await response.json() except aiohttp.ClientError as e: logger.debug(f"Gateway request error: {method} {url} - {e}") @@ -169,18 +199,23 @@ async def _request(self, method: str, path: str, params: Dict = None, json: Dict logger.debug(f"Gateway request failed: {method} {url} - {e}") raise - async def _get_error_body(self, response: aiohttp.ClientResponse) -> str: - """Extract error message from response body""" + async def _get_error_body(self, response: aiohttp.ClientResponse) -> tuple: + """Extract (message, code) from an error response body. + + Gateway's error envelope is {statusCode, error, message, code?}; the + code is machine-readable (TRANSACTION_TIMEOUT, SLIPPAGE_EXCEEDED, ...) + and is what callers should branch on. + """ try: data = await response.json() if isinstance(data, dict): - return data.get("message") or data.get("error") or str(data) - return str(data) + return (data.get("message") or data.get("error") or str(data), data.get("code")) + return (str(data), None) except Exception: try: - return await response.text() + return (await response.text(), None) except Exception: - return f"HTTP {response.status}" + return (f"HTTP {response.status}", None) async def ping(self) -> bool: """Check if Gateway is online""" @@ -195,13 +230,18 @@ async def get_wallets(self) -> List[Dict]: return await self._request("GET", "wallet") async def get_default_wallet_address(self, chain: str) -> Optional[str]: - """Get default wallet address for a chain from Gateway config""" - try: - config = await self._request("GET", "config", params={"namespace": chain}) - return config.get("defaultWallet") - except Exception as e: - logger.error(f"Error getting default wallet for chain {chain}: {e}") - return None + """Get default wallet address for a chain from Gateway config. + + Raises GatewayError(503) when Gateway is unreachable — an unreachable + Gateway must not masquerade as "no wallet configured" (a 400 that sends + the operator chasing the wrong problem). + """ + config = await self._request("GET", "config", params={"namespace": chain}) + if config is None: + raise GatewayError("Gateway service is not available", status=503) + if isinstance(config, dict) and set(config.keys()) == {"error", "status"}: + raise GatewayError(str(config["error"]), status=config.get("status", 502)) + return config.get("defaultWallet") async def get_all_wallet_addresses(self, chain: Optional[str] = None) -> Dict[str, List[str]]: """ @@ -226,7 +266,9 @@ async def get_all_wallet_addresses(self, chain: Optional[str] = None) -> Dict[st if chain and wallet_chain != chain: continue - addresses = wallet.get("walletAddresses", []) + # Hardware (Ledger) wallets live in a separate list — without them + # the discovery/balance sweeps never see hardware-held positions. + addresses = list(wallet.get("walletAddresses", [])) + list(wallet.get("hardwareWalletAddresses", [])) if addresses and wallet_chain: result[wallet_chain] = addresses @@ -419,18 +461,43 @@ async def delete_pool(self, chain: str, network: str, address: str) -> Dict: # Swap Operations (unified /trading/swap endpoints) # ============================================ - @staticmethod - def normalize_swap_connector(connector: str) -> str: + async def _get_connector_trading_types(self) -> Dict[str, List[str]]: + """Gateway's connector -> trading_types map, fetched once per client.""" + if self._connector_trading_types is None: + listing = check_gateway_error(await self.get_connectors()) + self._connector_trading_types = { + entry["name"]: list(entry.get("trading_types", [])) + for entry in listing.get("connectors", []) + if entry.get("name") + } + return self._connector_trading_types + + async def normalize_swap_connector(self, connector: str) -> str: """ Normalize a connector name to Gateway's 'name/type' swap-provider format. - 'jupiter' -> 'jupiter/router', 'meteora' -> 'meteora/clmm', - 'raydium/amm' -> 'raydium/amm' (already typed, passed through). + An already-typed value passes through untouched ('raydium/amm'). A bare + name takes the connector's most swap-appropriate trading type as Gateway + reports it: router, else clmm, else amm. Unknown names raise rather than + guessing a type that Gateway would reject with an opaque 400. """ if "/" in connector: return connector - connector_type = "router" if connector in ROUTER_CONNECTORS else "clmm" - return f"{connector}/{connector_type}" + trading_types = (await self._get_connector_trading_types()).get(connector) + if trading_types is None: + raise GatewayError( + f"Unknown swap connector '{connector}'. Gateway reports: " + f"{', '.join(sorted((await self._get_connector_trading_types()).keys()))}", + 400, + ) + for candidate in _SWAP_TYPE_PREFERENCE: + if candidate in trading_types: + return f"{connector}/{candidate}" + raise GatewayError( + f"Connector '{connector}' supports no swap trading type " + f"(Gateway reports: {', '.join(trading_types) or 'none'})", + 400, + ) async def quote_swap( self, @@ -440,7 +507,8 @@ async def quote_swap( quote_asset: str, amount: float, side: str, - slippage_pct: Optional[float] = None + slippage_pct: Optional[float] = None, + extra_params: Optional[Dict] = None ) -> Dict: """ Get a swap quote via Gateway's unified /trading/swap/quote endpoint. @@ -450,10 +518,12 @@ async def quote_swap( ('jupiter/router', 'raydium/amm', 'meteora/clmm'). chain_network: 'chain-network' format (e.g. 'solana-mainnet-beta'). For amm/clmm providers Gateway resolves the pool from its pool list. + extra_params: Connector-specific query params under Gateway's own names + (e.g. approximateIfNoExactOut). The router validates keys first. """ params = { "chainNetwork": chain_network, - "connector": self.normalize_swap_connector(connector), + "connector": await self.normalize_swap_connector(connector), "baseToken": base_asset, "quoteToken": quote_asset, "amount": str(amount), @@ -461,6 +531,11 @@ async def quote_swap( } if slippage_pct is not None: params["slippagePct"] = str(slippage_pct) + if extra_params: + # Query params must be strings for aiohttp; Gateway's schema coerces + # "true"/"false" back to booleans. + for key, value in extra_params.items(): + params[key] = str(value).lower() if isinstance(value, bool) else str(value) return await self._request("GET", "trading/swap/quote", params=params) @@ -473,12 +548,17 @@ async def execute_swap( quote_asset: str, amount: float, side: str, - slippage_pct: Optional[float] = None + slippage_pct: Optional[float] = None, + extra_params: Optional[Dict] = None ) -> Dict: - """Execute a swap via Gateway's unified /trading/swap/execute endpoint.""" + """Execute a swap via Gateway's unified /trading/swap/execute endpoint. + + extra_params carries connector-specific params under Gateway's own names + (e.g. approximateIfNoExactOut). The router validates keys first. + """ payload = { "chainNetwork": chain_network, - "connector": self.normalize_swap_connector(connector), + "connector": await self.normalize_swap_connector(connector), "walletAddress": wallet_address, "baseToken": base_asset, "quoteToken": quote_asset, @@ -487,6 +567,8 @@ async def execute_swap( } if slippage_pct is not None: payload["slippagePct"] = slippage_pct + if extra_params: + payload.update(extra_params) return await self._request("POST", "trading/swap/execute", json=payload) @@ -537,7 +619,8 @@ async def clmm_add_liquidity( position_address: str, base_token_amount: Optional[float] = None, quote_token_amount: Optional[float] = None, - slippage_pct: Optional[float] = None + slippage_pct: Optional[float] = None, + extra_params: Optional[Dict] = None ) -> Dict: """Add more liquidity to an existing CLMM position""" payload = { @@ -553,6 +636,10 @@ async def clmm_add_liquidity( if slippage_pct is not None: payload["slippagePct"] = slippage_pct + # Connector-specific parameters (e.g. Meteora's strategyType) + if extra_params: + payload.update(extra_params) + return await self._request("POST", "trading/clmm/add", json=payload) async def clmm_close_position( @@ -576,16 +663,24 @@ async def clmm_remove_liquidity( chain_network: str, wallet_address: str, position_address: str, - percentage: float + percentage_to_remove: float, + slippage_pct: Optional[float] = None ) -> Dict: - """Remove liquidity from a CLMM position (partial)""" - return await self._request("POST", "trading/clmm/remove", json={ + """Remove liquidity from a CLMM position (partial). + + slippage_pct is only honored by the Orca connector; others ignore it. + """ + payload = { "connector": connector, "chainNetwork": chain_network, "walletAddress": wallet_address, "positionAddress": position_address, - "percentageToRemove": percentage - }) + "percentageToRemove": percentage_to_remove + } + if slippage_pct is not None: + payload["slippagePct"] = slippage_pct + + return await self._request("POST", "trading/clmm/remove", json=payload) async def clmm_position_info( self, @@ -618,18 +713,19 @@ async def clmm_positions_owned( self, connector: str, chain_network: str, - wallet_address: str, - pool_address: Optional[str] = None + wallet_address: str ) -> List[Dict]: """ - Get CLMM positions owned by a wallet. + Get ALL CLMM positions owned by a wallet on a connector. + + Gateway's /trading/clmm/positions-owned takes no pool filter (its handler + reads only connector, chainNetwork and walletAddress); callers that care + about one pool filter the returned rows by their poolAddress field. Args: connector: CLMM connector (e.g., 'meteora', 'raydium') chain_network: Chain and network in format 'chain-network' (e.g., 'solana-mainnet-beta') wallet_address: Wallet address to query - pool_address: Optional pool address to filter positions. - If not provided, returns ALL positions across all pools. Returns: List of position dictionaries with fields like: @@ -646,12 +742,64 @@ async def clmm_positions_owned( "chainNetwork": chain_network, "walletAddress": wallet_address, } + return await self._request("GET", "trading/clmm/positions-owned", params=params) - # Only add poolAddress if specified (allows fetching all positions) - if pool_address: - params["poolAddress"] = pool_address + async def clmm_quote_position( + self, + connector: str, + chain_network: str, + pool_address: str, + lower_price: float, + upper_price: float, + base_token_amount: Optional[float] = None, + quote_token_amount: Optional[float] = None, + slippage_pct: Optional[float] = None, + ) -> Dict: + """Quote the base/quote split a candidate position would take, without signing anything.""" + params = { + "connector": connector, + "chainNetwork": chain_network, + "poolAddress": pool_address, + "lowerPrice": lower_price, + "upperPrice": upper_price, + } + if base_token_amount is not None: + params["baseTokenAmount"] = base_token_amount + if quote_token_amount is not None: + params["quoteTokenAmount"] = quote_token_amount + if slippage_pct is not None: + params["slippagePct"] = slippage_pct + return await self._request("GET", "trading/clmm/quote-position", params=params) - return await self._request("GET", "trading/clmm/positions-owned", params=params) + async def clmm_create_pool( + self, + connector: str, + chain_network: str, + wallet_address: str, + base_token: str, + quote_token: str, + initial_price: Optional[float] = None, + extra_params: Optional[Dict] = None, + ) -> Dict: + """Create a new (empty) CLMM pool. + + extra_params carries the connector-specific create params under Gateway's own + names (binStep, feeBps, ammConfigIndex) and is spread into the payload — the + same contract as clmm open's extra_params. The router validates keys before + this is called. + """ + payload = { + "connector": connector, + "chainNetwork": chain_network, + "walletAddress": wallet_address, + "baseToken": base_token, + "quoteToken": quote_token, + } + if initial_price is not None: + payload["initialPrice"] = initial_price + if extra_params: + payload.update(extra_params) + return await self._request("POST", "trading/clmm/create-pool", json=payload) async def clmm_collect_fees( self, @@ -672,43 +820,62 @@ async def clmm_pool_info( self, connector: str, chain_network: str, - pool_address: str + pool_address: str, + bin_count: int = 0 ) -> Dict: - """Get detailed CLMM pool information by pool address""" - return await self._request("GET", "trading/clmm/pool-info", params={ + """Get detailed CLMM pool information by pool address. + + bin_count > 0 asks Gateway for the per-tick liquidity distribution + (`bins`) around the active tick. Meteora always returns its bins and + ignores the parameter; orca, raydium, uniswap and pancakeswap honour it. + """ + params = { "connector": connector, "chainNetwork": chain_network, "poolAddress": pool_address - }) + } + if bin_count: + params["binCount"] = bin_count + return await self._request("GET", "trading/clmm/pool-info", params=params) async def clmm_fetch_pools( self, connector: str, network: str, - page: int = 0, limit: int = 50, query: Optional[str] = None, sort_by: Optional[str] = None, - include_unverified: bool = True + page: Optional[int] = None, + include_unverified: Optional[bool] = None, + sort_direction: Optional[str] = None, + verified_only: Optional[bool] = None ) -> Dict: """ Discover CLMM pools from the connector's own listing API (meteora, orca). This is a per-connector Gateway route (no unified equivalent): it proxies the - DEX's pool-discovery API rather than Gateway's saved pool list. + DEX's pool-discovery API rather than Gateway's saved pool list. The schemas + differ per connector — meteora takes page/includeUnverified and a + "field:direction" sortBy; orca takes sortDirection/verifiedOnly and does not + paginate. Only keys the caller sets are sent; AJV strips unknown keys + Gateway-side, so sending the wrong connector's knob would be a silent no-op. """ params = { "network": network, "limit": limit, } - if page > 0: - params["page"] = page if query: params["query"] = query if sort_by: params["sortBy"] = sort_by - if not include_unverified: - params["includeUnverified"] = "false" + if page is not None and page > 0: + params["page"] = page + if include_unverified is not None: + params["includeUnverified"] = "true" if include_unverified else "false" + if sort_direction: + params["sortDirection"] = sort_direction + if verified_only is not None: + params["verifiedOnly"] = "true" if verified_only else "false" return await self._request("GET", f"connectors/{connector}/clmm/fetch-pools", params=params) @@ -748,54 +915,6 @@ async def amm_positions_owned( "walletAddress": wallet_address, }) - async def amm_quote_swap( - self, - connector: str, - chain_network: str, - pool_address: str, - base_token: str, - side: str, - amount: float, - slippage_pct: Optional[float] = None, - ) -> Dict: - """Quote a swap against a specific AMM pool.""" - params = { - "connector": connector, - "chainNetwork": chain_network, - "poolAddress": pool_address, - "baseToken": base_token, - "side": side, - "amount": amount, - } - if slippage_pct is not None: - params["slippagePct"] = slippage_pct - return await self._request("GET", "trading/amm/quote-swap", params=params) - - async def amm_execute_swap( - self, - connector: str, - chain_network: str, - wallet_address: str, - pool_address: str, - base_token: str, - side: str, - amount: float, - slippage_pct: Optional[float] = None, - ) -> Dict: - """Execute a swap against a specific AMM pool.""" - payload = { - "connector": connector, - "chainNetwork": chain_network, - "walletAddress": wallet_address, - "poolAddress": pool_address, - "baseToken": base_token, - "side": side, - "amount": amount, - } - if slippage_pct is not None: - payload["slippagePct"] = slippage_pct - return await self._request("POST", "trading/amm/execute-swap", json=payload) - async def amm_quote_liquidity( self, connector: str, @@ -877,12 +996,17 @@ async def amm_create_pool( base_token_amount: float, quote_token_amount: Optional[float] = None, initial_price: Optional[float] = None, - config_address: Optional[str] = None, - fee_config_index: Optional[int] = None, - gas_price: Optional[float] = None, - max_gas: Optional[int] = None, + slippage_pct: Optional[float] = None, + extra_params: Optional[Dict] = None, ) -> Dict: - """Create and seed a new AMM pool. Connector extras are sent only when provided.""" + """Create and seed a new AMM pool. + + slippage_pct is the seeding slippage (uniswap/pancakeswap only); omitted, the + connector's configured slippagePct applies. extra_params carries the + connector-specific create params under Gateway's own names (configAddress, + ammConfigIndex) and is spread into the payload — the same contract as clmm + open's extra_params. The router validates keys before this is called. + """ payload = { "connector": connector, "chainNetwork": chain_network, @@ -896,15 +1020,10 @@ async def amm_create_pool( payload["quoteTokenAmount"] = quote_token_amount if initial_price is not None: payload["initialPrice"] = initial_price - # Connector-specific extras (each consumed only by its owning connector): - if config_address is not None: - payload["configAddress"] = config_address - if fee_config_index is not None: - payload["feeConfigIndex"] = fee_config_index - if gas_price is not None: - payload["gasPrice"] = gas_price - if max_gas is not None: - payload["maxGas"] = max_gas + if slippage_pct is not None: + payload["slippagePct"] = slippage_pct + if extra_params: + payload.update(extra_params) return await self._request("POST", "trading/amm/create-pool", json=payload) # ============================================ @@ -925,7 +1044,9 @@ async def poll_transaction( Returns: Transaction status dict with fields: - - txStatus: 1 for confirmed, 0 for pending, -1 for failed + - txStatus: 1 confirmed, 0 pending, -1 failed, -2 not found + (-2 is terminal on Solana once the blockhash expires — treat it + as dropped, not merely pending) - fee: Transaction fee amount - error: Parsed error message if transaction failed (e.g., "SLIPPAGE_EXCEEDED (0x1771): ...") - txData: Full transaction data including meta.err diff --git a/services/gateway_transaction_poller.py b/services/gateway_transaction_poller.py index 2781043e..4589203c 100644 --- a/services/gateway_transaction_poller.py +++ b/services/gateway_transaction_poller.py @@ -15,7 +15,7 @@ from database import AsyncDatabaseManager from database.models import GatewayCLMMPosition from database.repositories import GatewayCLMMRepository, GatewaySwapRepository -from services.gateway_client import GatewayClient +from services.gateway_client import GatewayClient, get_native_gas_token logger = logging.getLogger(__name__) @@ -48,6 +48,10 @@ def __init__( self._poll_task: Optional[asyncio.Task] = None self._position_poll_task: Optional[asyncio.Task] = None self._last_position_poll: Optional[datetime] = None + # Consecutive position-info misses per position. A single 404/500 can be a + # transient RPC problem (Gateway 500s on upstream hiccups), so a position is + # only marked CLOSED after MISSING_STRIKES_TO_CLOSE consecutive misses. + self._position_missing_strikes: Dict[str, int] = {} async def start(self): """Start the polling service.""" @@ -99,9 +103,28 @@ async def _poll_loop(self): except asyncio.CancelledError: break + # A tx reported NOT_FOUND (-2) is only terminal once its blockhash can no longer + # be valid (~90s on Solana); before that, -2 can just mean "not visible yet". + DROPPED_GRACE_SECONDS = 180 + + # Consecutive position-info misses before a position is marked CLOSED + # (mirrors the lp_executor's external-close gate). + MISSING_STRIKES_TO_CLOSE = 3 + + # Don't reopen a DB-CLOSED position seen in positions-owned if it was closed + # within this window: the listing RPC node may simply lag the close. + REOPEN_GRACE_SECONDS = 300 + async def _poll_pending_transactions(self): """Poll all pending transactions and update their status.""" try: + # One availability gate per cycle. When Gateway is unreachable nothing is + # polled AND nothing is aged out: the age timeout must never fire on a + # transaction we could not actually check (it may have confirmed on-chain). + if not await self.gateway_client.ping(): + logger.warning("Gateway not available; skipping transaction poll cycle") + return + async with self.db_manager.get_session_context() as session: swap_repo = GatewaySwapRepository(session) clmm_repo = GatewayCLMMRepository(session) @@ -111,18 +134,6 @@ async def _poll_pending_transactions(self): logger.debug(f"Found {len(pending_swaps)} pending swaps") for swap in pending_swaps: - # Skip if too old (likely failed without proper error) - age = (datetime.now(timezone.utc) - swap.timestamp).total_seconds() - if age > self.max_retry_age: - logger.warning(f"Swap {swap.transaction_hash} exceeded max retry age, marking as FAILED") - await swap_repo.update_swap_status( - transaction_hash=swap.transaction_hash, - status="FAILED", - error_message="Transaction confirmation timeout" - ) - continue - - # Poll transaction status await self._poll_swap_transaction(swap, swap_repo) # Get pending CLMM events @@ -130,18 +141,6 @@ async def _poll_pending_transactions(self): logger.debug(f"Found {len(pending_events)} pending CLMM events") for event in pending_events: - # Skip if too old - age = (datetime.now(timezone.utc) - event.timestamp).total_seconds() - if age > self.max_retry_age: - logger.warning(f"CLMM event {event.transaction_hash} exceeded max retry age, marking as FAILED") - await clmm_repo.update_event_status( - transaction_hash=event.transaction_hash, - status="FAILED", - error_message="Transaction confirmation timeout" - ) - continue - - # Poll transaction status await self._poll_clmm_event_transaction(event, clmm_repo) except Exception as e: @@ -158,31 +157,59 @@ async def _poll_swap_transaction(self, swap, swap_repo: GatewaySwapRepository): chain, network = parts - # Check transaction status on Gateway/blockchain - # Note: This is a placeholder - actual implementation depends on Gateway API status_result = await self._check_transaction_status( chain=chain, network=network, tx_hash=swap.transaction_hash ) + if status_result is None: + # Transient (Gateway/RPC hiccup): no information, no state change. + return - if status_result: - if status_result["status"] == "CONFIRMED": - logger.info(f"Swap transaction confirmed: {swap.transaction_hash}") - await swap_repo.update_swap_status( - transaction_hash=swap.transaction_hash, - status="CONFIRMED", - gas_fee=Decimal(str(status_result.get("gas_fee", 0))) if status_result.get("gas_fee") else None, - gas_token=status_result.get("gas_token") - ) - elif status_result["status"] == "FAILED": - logger.warning(f"Swap transaction failed: {swap.transaction_hash}") - await swap_repo.update_swap_status( - transaction_hash=swap.transaction_hash, - status="FAILED", - error_message=status_result.get("error_message", "Transaction failed on-chain") - ) - # If status is still pending, do nothing and retry later + age = (datetime.now(timezone.utc) - swap.timestamp).total_seconds() + status = status_result["status"] + gas_fee_raw = status_result.get("gas_fee") + gas_fee = Decimal(str(gas_fee_raw)) if gas_fee_raw is not None else None + + if status == "CONFIRMED": + # Accepted residual: amounts/price are NOT backfilled from the poll's + # txData — a swap recorded while pending keeps its request-side leg + # and 0 placeholders after confirmation. Backfilling requires parsing + # balance changes from txData (deferred by design). + logger.info(f"Swap transaction confirmed: {swap.transaction_hash}") + await swap_repo.update_swap_status( + transaction_hash=swap.transaction_hash, + status="CONFIRMED", + gas_fee=gas_fee, + gas_token=status_result.get("gas_token") + ) + elif status == "FAILED": + # A landed-but-failed tx still paid gas — record it. + logger.warning(f"Swap transaction failed: {swap.transaction_hash}") + await swap_repo.update_swap_status( + transaction_hash=swap.transaction_hash, + status="FAILED", + error_message=status_result.get("error_message", "Transaction failed on-chain"), + gas_fee=gas_fee, + gas_token=status_result.get("gas_token") + ) + elif status == "DROPPED" and age > self.DROPPED_GRACE_SECONDS: + logger.warning(f"Swap transaction dropped (not found on-chain): {swap.transaction_hash}") + await swap_repo.update_swap_status( + transaction_hash=swap.transaction_hash, + status="FAILED", + error_message="Transaction not found on-chain (dropped after blockhash expiry)" + ) + elif status == "PENDING" and age > self.max_retry_age: + # Genuinely still unconfirmed after a successful poll — only now may + # the age timeout fire. + logger.warning(f"Swap {swap.transaction_hash} exceeded max retry age, marking as FAILED") + await swap_repo.update_swap_status( + transaction_hash=swap.transaction_hash, + status="FAILED", + error_message="Transaction confirmation timeout" + ) + # PENDING within age / DROPPED within grace: retry next cycle. except Exception as e: logger.error(f"Error polling swap transaction {swap.transaction_hash}: {e}") @@ -205,33 +232,54 @@ async def _poll_clmm_event_transaction(self, event, clmm_repo: GatewayCLMMReposi chain, network = parts - # Check transaction status status_result = await self._check_transaction_status( chain=chain, network=network, tx_hash=event.transaction_hash ) + if status_result is None: + # Transient (Gateway/RPC hiccup): no information, no state change. + return - if status_result: - if status_result["status"] == "CONFIRMED": - logger.info(f"CLMM event transaction confirmed: {event.transaction_hash}") - await clmm_repo.update_event_status( - transaction_hash=event.transaction_hash, - status="CONFIRMED", - gas_fee=Decimal(str(status_result.get("gas_fee", 0))) if status_result.get("gas_fee") else None, - gas_token=status_result.get("gas_token") - ) - - # Update position state based on event type - await self._update_position_from_event(event, clmm_repo) - - elif status_result["status"] == "FAILED": - logger.warning(f"CLMM event transaction failed: {event.transaction_hash}") - await clmm_repo.update_event_status( - transaction_hash=event.transaction_hash, - status="FAILED", - error_message=status_result.get("error_message", "Transaction failed on-chain") - ) + age = (datetime.now(timezone.utc) - event.timestamp).total_seconds() + status = status_result["status"] + gas_fee_raw = status_result.get("gas_fee") + gas_fee = Decimal(str(gas_fee_raw)) if gas_fee_raw is not None else None + + if status == "CONFIRMED": + logger.info(f"CLMM event transaction confirmed: {event.transaction_hash}") + await clmm_repo.update_event_status( + transaction_hash=event.transaction_hash, + status="CONFIRMED", + gas_fee=gas_fee, + gas_token=status_result.get("gas_token") + ) + # Update position state based on event type + await self._update_position_from_event(event, clmm_repo) + elif status == "FAILED": + logger.warning(f"CLMM event transaction failed: {event.transaction_hash}") + await clmm_repo.update_event_status( + transaction_hash=event.transaction_hash, + status="FAILED", + error_message=status_result.get("error_message", "Transaction failed on-chain"), + gas_fee=gas_fee, + gas_token=status_result.get("gas_token") + ) + elif status == "DROPPED" and age > self.DROPPED_GRACE_SECONDS: + logger.warning(f"CLMM event transaction dropped (not found on-chain): {event.transaction_hash}") + await clmm_repo.update_event_status( + transaction_hash=event.transaction_hash, + status="FAILED", + error_message="Transaction not found on-chain (dropped after blockhash expiry)" + ) + elif status == "PENDING" and age > self.max_retry_age: + logger.warning(f"CLMM event {event.transaction_hash} exceeded max retry age, marking as FAILED") + await clmm_repo.update_event_status( + transaction_hash=event.transaction_hash, + status="FAILED", + error_message="Transaction confirmation timeout" + ) + # PENDING within age / DROPPED within grace: retry next cycle. except Exception as e: logger.error(f"Error polling CLMM event transaction {event.transaction_hash}: {e}") @@ -247,10 +295,37 @@ async def _update_position_from_event(self, event, clmm_repo: GatewayCLMMReposit return if event.event_type == "CLOSE": + # Fee booking happens exactly once, on confirmation: the endpoints + # only mutate the position when Gateway confirmed the tx inline, and + # leave submitted-not-confirmed booking to this path. + if event.base_fee_collected is not None or event.quote_fee_collected is not None: + new_base = float(position.base_fee_collected or 0) + float(event.base_fee_collected or 0) + new_quote = float(position.quote_fee_collected or 0) + float(event.quote_fee_collected or 0) + await clmm_repo.update_position_fees( + position_address=position.position_address, + base_fee_collected=Decimal(str(new_base)), + quote_fee_collected=Decimal(str(new_quote)), + base_fee_pending=Decimal("0"), + quote_fee_pending=Decimal("0") + ) await clmm_repo.close_position(position.position_address) + elif event.event_type == "ADD_LIQUIDITY": + # Added capital raises the PnL baseline. Event amounts may be the + # requested figures (recorded at submit time) rather than on-chain + # actuals — the accepted residual is that pending-tx amounts are not + # backfilled from txData; requested amounts are the best available. + if event.base_token_amount or event.quote_token_amount: + await clmm_repo.add_to_initial_amounts( + position_address=position.position_address, + base_delta=Decimal(str(event.base_token_amount or 0)), + quote_delta=Decimal(str(event.quote_token_amount or 0)), + ) + elif event.event_type == "COLLECT_FEES": - # Add collected fees to cumulative total + # Add collected fees to cumulative total (endpoints book inline only + # for txs Gateway confirmed at submit time — those events are created + # CONFIRMED and never reach this path, so there is no double count). if event.base_fee_collected or event.quote_fee_collected: new_base_collected = float(position.base_fee_collected or 0) + float(event.base_fee_collected or 0) new_quote_collected = float(position.quote_fee_collected or 0) + float(event.quote_fee_collected or 0) @@ -276,16 +351,14 @@ async def _check_transaction_status( Check transaction status on blockchain via Gateway. Returns: - Dict with status, gas_fee, gas_token, and error_message if available. - None if transaction not yet confirmed or pending. + Dict with status ("CONFIRMED" | "FAILED" | "DROPPED" | "PENDING"), + gas_fee, gas_token, and error_message. + None only when no information could be obtained (Gateway/RPC hiccup) — + callers must treat that as "no state change", never as pending-with-age. """ try: - # Check if Gateway is available - if not await self.gateway_client.ping(): - logger.warning("Gateway not available for transaction polling") - return None - # Reconstruct network_id from chain and network + # (Gateway availability is gated once per cycle by the caller.) network_id = f"{chain}-{network}" # Poll transaction status from Gateway @@ -310,80 +383,79 @@ async def _check_transaction_status( logger.debug(f"Polled transaction {tx_hash} on {network_id}: txStatus={result.get('txStatus')}") - # Parse the response with defensive checks + # Classify on txStatus ALONE. Gateway deliberately returns txStatus 0 + # (pending) WITH a non-null `error` for transient poll failures — "the + # caller should poll again, not give up" — so the error field must never + # promote a pending transaction to FAILED. tx_status = result.get("txStatus") + gas_token = get_native_gas_token(chain) + gas_fee = result.get("fee") - # Determine gas token based on chain - gas_token = { - "solana": "SOL", - "ethereum": "ETH", - "arbitrum": "ETH", - "optimism": "ETH", - "polygon": "MATIC", - "avalanche": "AVAX" - }.get(chain, "UNKNOWN") - - # Transaction is confirmed if txStatus == 1 if tx_status == 1: return { "status": "CONFIRMED", - "gas_fee": result.get("fee", 0), + "gas_fee": gas_fee, "gas_token": gas_token, "error_message": None } - # Transaction failed if txStatus == -1 or there's an error field - # Gateway now returns parsed error messages like "SLIPPAGE_EXCEEDED (0x1771): ..." - error_msg = result.get("error") - if tx_status == -1 or error_msg: + if tx_status == -1: + # Landed on-chain but failed. Gateway returns parsed error messages + # like "SLIPPAGE_EXCEEDED (0x1771): ..."; fall back to meta.err. + error_msg = result.get("error") if not error_msg: - # Fallback to meta.err if no parsed error tx_data = result.get("txData") or {} meta = tx_data.get("meta") if isinstance(tx_data, dict) else {} raw_error = meta.get("err") if isinstance(meta, dict) else None error_msg = str(raw_error) if raw_error else "Transaction failed on-chain" return { "status": "FAILED", - "gas_fee": result.get("fee", 0), + "gas_fee": gas_fee, "gas_token": gas_token, "error_message": error_msg } - # Transaction still pending (txStatus == 0 or not finalized) - return None + if tx_status == -2: + # NOT_FOUND — terminal on Solana once the blockhash expires; can also + # appear briefly right after submission. The caller applies the grace + # window before treating it as dropped. + return { + "status": "DROPPED", + "gas_fee": None, + "gas_token": gas_token, + "error_message": "Transaction not found on-chain" + } + + # txStatus 0 (or anything unrecognized): a successful poll that says the + # transaction is still unconfirmed — distinct from None (no information). + return { + "status": "PENDING", + "gas_fee": None, + "gas_token": gas_token, + "error_message": None + } except Exception as e: logger.error(f"Error checking transaction status for {tx_hash}: {e}") return None - async def poll_transaction_once(self, tx_hash: str, network_id: str) -> Optional[Dict]: - """ - Poll a specific transaction once (useful for immediate status checks). - - Args: - tx_hash: Transaction hash - network_id: Network ID in format 'chain-network' (e.g., 'solana-mainnet-beta') - - Returns: - Transaction status dict or None if pending - """ - parts = network_id.split('-', 1) - if len(parts) != 2: - logger.error(f"Invalid network format: {network_id}") - return None - - chain, network = parts - return await self._check_transaction_status(chain, network, tx_hash) - # ============================================ # Position State Polling & Discovery # ============================================ - # Supported CLMM connectors and their default networks + # Supported CLMM connectors and their default networks. The discovery sweep is + # also the reconciliation path for opens that returned submitted-not-confirmed + # (position_address unknown at open time), so every Solana connector whose open + # can pend (meteora, raydium, pancakeswap-sol) must be listed; orca rides along + # to reconcile externally-created positions. All four speak the unified + # /trading/clmm/positions-owned schema. SUPPORTED_CLMM_CONFIGS = [ {"connector": "meteora", "chain": "solana", "network": "mainnet-beta"}, - # Add more connectors as they become supported: - # {"connector": "raydium", "chain": "solana", "network": "mainnet-beta"}, + {"connector": "raydium", "chain": "solana", "network": "mainnet-beta"}, + {"connector": "pancakeswap-sol", "chain": "solana", "network": "mainnet-beta"}, + {"connector": "orca", "chain": "solana", "network": "mainnet-beta"}, + # EVM CLMM opens never return the pending shape (data always present), so + # discovery is not load-bearing there: # {"connector": "uniswap", "chain": "ethereum", "network": "mainnet"}, ] @@ -461,6 +533,8 @@ async def _discover_positions_from_gateway(self) -> int: open_positions = await clmm_repo.get_position_addresses_set(status="OPEN") # Get CLOSED positions (to potentially reopen if still on-chain) closed_positions = await clmm_repo.get_position_addresses_set(status="CLOSED") + # Positions closed moments ago are exempt from reopening (lag guard) + recently_closed = await clmm_repo.get_recently_closed_addresses(self.REOPEN_GRACE_SECONDS) # Poll each supported connector/chain/wallet combination for config in self.SUPPORTED_CLMM_CONFIGS: @@ -480,8 +554,7 @@ async def _discover_positions_from_gateway(self) -> int: gateway_positions = await self.gateway_client.clmm_positions_owned( connector=connector, chain_network=chain_network, - wallet_address=wallet_address, - pool_address=None # Get all positions across all pools + wallet_address=wallet_address ) if not gateway_positions or not isinstance(gateway_positions, list): @@ -499,6 +572,13 @@ async def _discover_positions_from_gateway(self) -> int: # Check if position was incorrectly marked as CLOSED if position_address in closed_positions: + if position_address in recently_closed: + # Just closed — the positions-owned RPC node may + # lag the close confirmation; reopening now would + # flap the record CLOSED -> OPEN -> CLOSED. + logger.debug(f"Position {position_address} closed recently; " + "skipping reopen (listing may lag the close)") + continue # Position exists on-chain but is CLOSED in DB → reopen it async with self.db_manager.get_session_context() as session: clmm_repo = GatewayCLMMRepository(session) @@ -509,7 +589,7 @@ async def _discover_positions_from_gateway(self) -> int: closed_positions.discard(position_address) open_positions.add(position_address) logger.warning(f"Reopened position {position_address} - " - f"was CLOSED in DB but still exists on-chain") + f"was CLOSED in DB but still exists on-chain") continue # Create new position in database @@ -525,7 +605,7 @@ async def _discover_positions_from_gateway(self) -> int: discovered_count += 1 open_positions.add(position_address) logger.info(f"Discovered new position: {position_address} " - f"(pool: {pos_data.get('poolAddress', 'unknown')[:16]}...)") + f"(pool: {pos_data.get('poolAddress', 'unknown')[:16]}...)") except Exception as e: logger.warning(f"Error discovering positions for {connector}/{chain}/{wallet_address}: {e}") @@ -677,11 +757,6 @@ async def _update_all_open_positions(self): except Exception as e: logger.error(f"Error updating open positions: {e}", exc_info=True) - # Legacy method name for backwards compatibility - async def _poll_open_positions(self): - """Poll all open CLMM positions and update their state. (Legacy wrapper)""" - await self._poll_and_discover_positions() - async def _refresh_position_state(self, position: GatewayCLMMPosition, clmm_repo: GatewayCLMMRepository): """ Refresh a single position's state from Gateway. @@ -728,14 +803,27 @@ async def _refresh_position_state(self, position: GatewayCLMMPosition, clmm_repo if "error" in result: status_code = result.get("status") - # Gateway returns 500 instead of 404 when position doesn't exist (closed) - # Treat any error (404 or 500) on position-info as "position closed" + # Some connectors 500 instead of 404 for a nonexistent position, + # but Gateway ALSO 500s on transient RPC problems — so a miss only + # counts as a strike, and the position closes after + # MISSING_STRIKES_TO_CLOSE consecutive misses, never on one. if status_code in (404, 500): - logger.info(f"Position {position.position_address} not found on Gateway (status: {status_code}), marking as CLOSED") - await clmm_repo.close_position(position.position_address) + strikes = self._position_missing_strikes.get(position.position_address, 0) + 1 + self._position_missing_strikes[position.position_address] = strikes + if strikes >= self.MISSING_STRIKES_TO_CLOSE: + logger.info(f"Position {position.position_address} missing from Gateway " + f"{strikes} consecutive times (last status: {status_code}), " + "marking as CLOSED") + await clmm_repo.close_position(position.position_address) + self._position_missing_strikes.pop(position.position_address, None) + else: + logger.debug(f"Position {position.position_address} miss " + f"{strikes}/{self.MISSING_STRIKES_TO_CLOSE} " + f"(status: {status_code}), not closing yet") return # Other errors → skip update, don't close - logger.debug(f"Gateway error for position {position.position_address}: {result.get('error')} (status: {status_code})") + logger.debug(f"Gateway error for position {position.position_address}: " + f"{result.get('error')} (status: {status_code})") return # Validate response has required fields @@ -743,6 +831,9 @@ async def _refresh_position_state(self, position: GatewayCLMMPosition, clmm_repo logger.warning(f"Invalid response for position {position.position_address}, missing 'address' field") return + # Successful read: the position exists — reset the miss counter. + self._position_missing_strikes.pop(position.position_address, None) + except Exception as e: logger.warning(f"Error fetching position {position.position_address} from Gateway: {e}") return @@ -798,8 +889,8 @@ async def _refresh_position_state(self, position: GatewayCLMMPosition, clmm_repo ) logger.debug(f"Refreshed position {position.position_address}: price={current_price}, in_range={in_range}, " - f"base={base_token_amount}, quote={quote_token_amount}, " - f"base_fee={base_fee_pending}, quote_fee={quote_fee_pending}") + f"base={base_token_amount}, quote={quote_token_amount}, " + f"base_fee={base_fee_pending}, quote_fee={quote_fee_pending}") except Exception as e: logger.error(f"Error refreshing position state {position.position_address}: {e}", exc_info=True) diff --git a/services/gateway_wallet_service.py b/services/gateway_wallet_service.py index c12d84e3..b06e07c1 100644 --- a/services/gateway_wallet_service.py +++ b/services/gateway_wallet_service.py @@ -5,7 +5,7 @@ from fastapi import HTTPException -from services.gateway_client import GatewayClient, check_gateway_error +from services.gateway_client import GatewayClient, GatewayError, check_gateway_error from services.gecko_price_source import GeckoPriceSource # Create module-specific logger @@ -74,11 +74,16 @@ async def get_gateway_wallets(self) -> List[Dict]: try: wallets = check_gateway_error(await self.gateway_client.get_wallets()) - # Enrich with default wallet info for each chain + # Enrich with default wallet info for each chain; a per-chain config + # error degrades that chain's default to "" rather than failing the list. for wallet_group in wallets: chain = wallet_group.get("chain") if chain: - default_wallet = await self.gateway_client.get_default_wallet_address(chain) + try: + default_wallet = await self.gateway_client.get_default_wallet_address(chain) + except GatewayError as e: + logger.warning(f"Could not read default wallet for {chain}: {e}") + default_wallet = None wallet_group["default_address"] = default_wallet or "" return wallets @@ -103,6 +108,9 @@ async def add_gateway_wallet(self, chain: str, private_key: str, set_default: bo try: result = await self.gateway_client.add_wallet(chain, private_key, set_default=set_default) + if result is None: + # Connection error: the client returns None — 503, not a crash on `in None`. + raise HTTPException(status_code=503, detail="Gateway service is not available") if "error" in result: raise HTTPException(status_code=400, detail=f"Gateway error: {result['error']}") @@ -131,6 +139,9 @@ async def remove_gateway_wallet(self, chain: str, address: str) -> Dict: try: result = await self.gateway_client.remove_wallet(chain, address) + if result is None: + # Connection error: the client returns None — 503, not a crash on `in None`. + raise HTTPException(status_code=503, detail="Gateway service is not available") if "error" in result: raise HTTPException(status_code=400, detail=f"Gateway error: {result['error']}") @@ -169,6 +180,8 @@ async def get_gateway_balances(self, chain: str, address: str, network: Optional # Get balances from Gateway balances_response = await self.gateway_client.get_balances(chain, network, address, tokens=tokens) + if balances_response is None: + raise HTTPException(status_code=503, detail="Gateway service is not available") if "error" in balances_response: raise HTTPException(status_code=400, detail=f"Gateway error: {balances_response['error']}") diff --git a/test/test_extra_params_validation.py b/test/test_extra_params_validation.py new file mode 100644 index 00000000..6d1bdf50 --- /dev/null +++ b/test/test_extra_params_validation.py @@ -0,0 +1,84 @@ +""" +Unit tests for routers.gateway_extras.validate_extra_params — the shared guard +that keeps connector-specific extra_params from being silently dropped or +misparsed by Gateway's unified /trading routes. +""" +import pytest +from fastapi import HTTPException + +from routers.gateway_extras import validate_extra_params + +SPEC = { + "approximateIfNoExactOut": ((bool,), {"jupiter", "dflow", "okx", "titan"}), + "strategyType": ((int,), {"meteora"}), + "configAddress": ((str,), {"meteora"}), + "feeBps": ((int, float), {"meteora", "uniswap"}), +} + + +def _detail(exc_info): + return exc_info.value.detail + + +def test_none_and_empty_pass(): + validate_extra_params(None, SPEC, "jupiter", "route") + validate_extra_params({}, SPEC, "jupiter", "route") + + +def test_valid_params_pass(): + validate_extra_params({"approximateIfNoExactOut": False}, SPEC, "jupiter", "route") + validate_extra_params({"strategyType": 0}, SPEC, "meteora", "route") + validate_extra_params({"feeBps": 0.25}, SPEC, "uniswap", "route") + + +def test_typed_provider_validates_on_base_name(): + validate_extra_params({"approximateIfNoExactOut": True}, SPEC, "jupiter/router", "route") + + +def test_unknown_key_rejected(): + with pytest.raises(HTTPException) as exc: + validate_extra_params({"gasPrice": 20}, SPEC, "uniswap", "the route") + assert exc.value.status_code == 400 + assert "gasPrice" in _detail(exc) + + +def test_wrong_connector_rejected(): + """A key the connector would silently ignore must 400, not pass.""" + with pytest.raises(HTTPException) as exc: + validate_extra_params({"strategyType": 0}, SPEC, "orca", "route") + assert exc.value.status_code == 400 + assert "orca" in _detail(exc) + + +def test_wrong_connector_rejected_for_typed_provider(): + with pytest.raises(HTTPException) as exc: + validate_extra_params({"approximateIfNoExactOut": True}, SPEC, "meteora/clmm", "route") + assert exc.value.status_code == 400 + + +def test_none_value_rejected(): + """A null value would reach Gateway as the string 'None' on GET paths.""" + with pytest.raises(HTTPException) as exc: + validate_extra_params({"approximateIfNoExactOut": None}, SPEC, "jupiter", "route") + assert exc.value.status_code == 400 + assert "bool" in _detail(exc) + + +def test_wrong_type_rejected(): + with pytest.raises(HTTPException) as exc: + validate_extra_params({"approximateIfNoExactOut": "yes"}, SPEC, "jupiter", "route") + assert exc.value.status_code == 400 + + +def test_bool_does_not_pass_as_int(): + """bool subclasses int — True must not satisfy an int-typed key.""" + with pytest.raises(HTTPException) as exc: + validate_extra_params({"strategyType": True}, SPEC, "meteora", "route") + assert exc.value.status_code == 400 + assert "int" in _detail(exc) + + +def test_int_does_not_pass_as_bool(): + with pytest.raises(HTTPException) as exc: + validate_extra_params({"approximateIfNoExactOut": 1}, SPEC, "jupiter", "route") + assert exc.value.status_code == 400 diff --git a/test/test_gateway_client_contract.py b/test/test_gateway_client_contract.py index 38d2a803..70e97de0 100644 --- a/test/test_gateway_client_contract.py +++ b/test/test_gateway_client_contract.py @@ -18,6 +18,22 @@ from services.gateway_client import GatewayClient, GatewayError, check_gateway_error +# Gateway's own config/connectors listing decides an untyped connector's swap +# type; these trading_types mirror what Gateway reports today. +_CONNECTOR_LISTING = {"connectors": [ + {"name": "jupiter", "trading_types": ["router"]}, + {"name": "0x", "trading_types": ["router"]}, + {"name": "uniswap", "trading_types": ["router", "amm", "clmm"]}, + {"name": "pancakeswap", "trading_types": ["router", "amm", "clmm"]}, + {"name": "dflow", "trading_types": ["router"]}, + {"name": "okx", "trading_types": ["router"]}, + {"name": "titan", "trading_types": ["router"]}, + {"name": "meteora", "trading_types": ["clmm", "amm"]}, + {"name": "orca", "trading_types": ["clmm"]}, + {"name": "raydium", "trading_types": ["clmm", "amm"]}, + {"name": "pancakeswap-sol", "trading_types": ["clmm"]}, +]} + @pytest.fixture def client_and_calls(monkeypatch): @@ -30,6 +46,11 @@ async def fake_request(method, path, params=None, json=None): return {} monkeypatch.setattr(client, "_request", fake_request) + # Pre-seed Gateway's connector listing so swap payload assertions see only + # the swap call itself, not the one-off discovery request behind it. + client._connector_trading_types = { + entry["name"]: entry["trading_types"] for entry in _CONNECTOR_LISTING["connectors"] + } return client, calls @@ -37,11 +58,16 @@ async def fake_request(method, path, params=None, json=None): # Connector normalization # ============================================ + +@pytest.mark.asyncio @pytest.mark.parametrize("connector,expected", [ ("jupiter", "jupiter/router"), ("0x", "0x/router"), ("uniswap", "uniswap/router"), ("pancakeswap", "pancakeswap/router"), + ("dflow", "dflow/router"), + ("okx", "okx/router"), + ("titan", "titan/router"), ("meteora", "meteora/clmm"), ("orca", "orca/clmm"), ("raydium", "raydium/clmm"), @@ -51,8 +77,21 @@ async def fake_request(method, path, params=None, json=None): ("meteora/clmm", "meteora/clmm"), ("raydium/amm", "raydium/amm"), ]) -def test_normalize_swap_connector(connector, expected): - assert GatewayClient.normalize_swap_connector(connector) == expected +async def test_normalize_swap_connector(connector, expected): + client = GatewayClient() + client._connector_trading_types = { + entry["name"]: entry["trading_types"] for entry in _CONNECTOR_LISTING["connectors"] + } + assert await client.normalize_swap_connector(connector) == expected + + +@pytest.mark.asyncio +async def test_normalize_swap_connector_rejects_unknown_name(): + client = GatewayClient() + client._connector_trading_types = {"jupiter": ["router"]} + with pytest.raises(GatewayError) as exc: + await client.normalize_swap_connector("nosuchdex") + assert "nosuchdex" in str(exc.value) # ============================================ @@ -110,6 +149,45 @@ async def test_quote_swap_zero_slippage_is_sent(client_and_calls): assert calls[0]["params"]["slippagePct"] == "0" +@pytest.mark.asyncio +async def test_quote_swap_omits_slippage_when_unset(client_and_calls): + """No slippage_pct => omit the key so Gateway applies the connector's configured default.""" + client, calls = client_and_calls + await client.quote_swap( + connector="jupiter", chain_network="solana-mainnet-beta", + base_asset="SOL", quote_asset="USDC", amount=1, side="SELL", + ) + assert "slippagePct" not in calls[0]["params"] + + +@pytest.mark.asyncio +async def test_quote_swap_extra_params_bool_as_query_string(client_and_calls): + """approximateIfNoExactOut rides extra_params; aiohttp needs query values as strings, + and Gateway's schema coerces 'false' back to boolean.""" + client, calls = client_and_calls + await client.quote_swap( + connector="jupiter", chain_network="solana-mainnet-beta", + base_asset="SOL", quote_asset="USDC", amount=1, side="BUY", + extra_params={"approximateIfNoExactOut": False}, + ) + assert calls[0]["params"]["approximateIfNoExactOut"] == "false" + + +@pytest.mark.asyncio +async def test_execute_swap_extra_params_and_slippage_omission(client_and_calls): + """extra_params keys land in the JSON body under Gateway's own names (booleans + intact); unset slippage_pct is omitted so the connector default applies.""" + client, calls = client_and_calls + await client.execute_swap( + connector="jupiter/router", chain_network="solana-mainnet-beta", + wallet_address="WALLET", base_asset="SOL", quote_asset="USDC", + amount=0.1, side="BUY", extra_params={"approximateIfNoExactOut": False}, + ) + body = calls[0]["json"] + assert body["approximateIfNoExactOut"] is False + assert "slippagePct" not in body + + # ============================================ # CLMM paths and payloads (unified /trading/clmm) # ============================================ @@ -151,17 +229,47 @@ async def test_clmm_add_liquidity_path_and_keys(client_and_calls): assert call["json"]["chainNetwork"] == "solana-mainnet-beta" +@pytest.mark.asyncio +async def test_clmm_add_liquidity_extra_params_and_slippage_omission(client_and_calls): + """strategyType rides extra_params into the body under Gateway's name; unset + slippage_pct is omitted so the connector default applies.""" + client, calls = client_and_calls + await client.clmm_add_liquidity( + connector="meteora", chain_network="solana-mainnet-beta", + wallet_address="WALLET", position_address="POS", + base_token_amount=0.5, quote_token_amount=50.0, + extra_params={"strategyType": 0}, + ) + body = calls[0]["json"] + assert body["strategyType"] == 0 + assert "slippagePct" not in body + + @pytest.mark.asyncio async def test_clmm_remove_liquidity_uses_percentage_to_remove(client_and_calls): client, calls = client_and_calls await client.clmm_remove_liquidity( connector="meteora", chain_network="solana-mainnet-beta", - wallet_address="WALLET", position_address="POS", percentage=50.0, + wallet_address="WALLET", position_address="POS", percentage_to_remove=50.0, ) call = calls[0] assert (call["method"], call["path"]) == ("POST", "trading/clmm/remove") assert call["json"]["percentageToRemove"] == 50.0 assert "percentage" not in call["json"] + # No slippage_pct given => omitted (Orca falls back to its configured default) + assert "slippagePct" not in call["json"] + + +@pytest.mark.asyncio +async def test_clmm_remove_liquidity_sends_slippage_when_set(client_and_calls): + """Orca honors slippagePct on remove; the client must forward it.""" + client, calls = client_and_calls + await client.clmm_remove_liquidity( + connector="orca", chain_network="solana-mainnet-beta", + wallet_address="WALLET", position_address="POS", percentage_to_remove=100.0, + slippage_pct=0.5, + ) + assert calls[0]["json"]["slippagePct"] == 0.5 @pytest.mark.asyncio @@ -200,11 +308,33 @@ async def test_clmm_pool_info_uses_unified_endpoint(client_and_calls): @pytest.mark.asyncio -async def test_clmm_fetch_pools_path(client_and_calls): +async def test_clmm_fetch_pools_meteora_params(client_and_calls): + """Meteora's fetch-pools paginates and filters via page/includeUnverified.""" client, calls = client_and_calls - await client.clmm_fetch_pools(connector="meteora", network="mainnet-beta", limit=10) + await client.clmm_fetch_pools(connector="meteora", network="mainnet-beta", limit=10, + sort_by="volume_24h:desc", page=2, include_unverified=False) call = calls[0] assert (call["method"], call["path"]) == ("GET", "connectors/meteora/clmm/fetch-pools") + assert call["params"]["page"] == 2 + assert call["params"]["includeUnverified"] == "false" + assert call["params"]["sortBy"] == "volume_24h:desc" + for orca_only in ("sortDirection", "verifiedOnly"): + assert orca_only not in call["params"] + + +@pytest.mark.asyncio +async def test_clmm_fetch_pools_orca_params(client_and_calls): + """Orca's fetch-pools takes sortDirection/verifiedOnly and has no pagination — + sending meteora's knobs would be silently stripped by Gateway's AJV.""" + client, calls = client_and_calls + await client.clmm_fetch_pools(connector="orca", network="mainnet-beta", limit=10, + sort_by="volume", sort_direction="desc", verified_only=True) + call = calls[0] + assert (call["method"], call["path"]) == ("GET", "connectors/orca/clmm/fetch-pools") + assert call["params"]["sortDirection"] == "desc" + assert call["params"]["verifiedOnly"] == "true" + for meteora_only in ("page", "includeUnverified"): + assert meteora_only not in call["params"] # ============================================ @@ -278,27 +408,24 @@ async def test_amm_positions_owned_path(client_and_calls): @pytest.mark.asyncio -async def test_amm_quote_swap_path_and_slippage_omitted(client_and_calls): +async def test_amm_quote_liquidity_path_and_slippage_omitted(client_and_calls): client, calls = client_and_calls - await client.amm_quote_swap(connector="raydium", chain_network=NET, pool_address=POOL, - base_token="SOL", side="SELL", amount=0.01) + await client.amm_quote_liquidity(connector="meteora", chain_network=NET, pool_address=POOL, + base_token_amount=1.0, quote_token_amount=100.0) c = calls[0] - assert (c["method"], c["path"]) == ("GET", "trading/amm/quote-swap") - assert c["params"] == {"connector": "raydium", "chainNetwork": NET, "poolAddress": POOL, - "baseToken": "SOL", "side": "SELL", "amount": 0.01} + assert (c["method"], c["path"]) == ("GET", "trading/amm/quote-liquidity") + assert c["params"] == {"connector": "meteora", "chainNetwork": NET, "poolAddress": POOL, + "baseTokenAmount": 1.0, "quoteTokenAmount": 100.0} + # Omitted slippage means "use the connector's configured slippagePct" assert "slippagePct" not in c["params"] @pytest.mark.asyncio -async def test_amm_execute_swap_path(client_and_calls): +async def test_amm_quote_liquidity_sends_zero_slippage(client_and_calls): client, calls = client_and_calls - await client.amm_execute_swap(connector="uniswap", chain_network="ethereum-mainnet", wallet_address=WALLET, - pool_address=POOL, base_token="WETH", side="BUY", amount=1.0, slippage_pct=0.5) - c = calls[0] - assert (c["method"], c["path"]) == ("POST", "trading/amm/execute-swap") - assert c["json"]["walletAddress"] == WALLET - assert c["json"]["chainNetwork"] == "ethereum-mainnet" - assert c["json"]["slippagePct"] == 0.5 + await client.amm_quote_liquidity(connector="meteora", chain_network=NET, pool_address=POOL, + base_token_amount=1.0, quote_token_amount=100.0, slippage_pct=0) + assert calls[0]["params"]["slippagePct"] == 0 @pytest.mark.asyncio @@ -344,35 +471,51 @@ async def test_amm_create_pool_meteora_extras(client_and_calls): client, calls = client_and_calls await client.amm_create_pool(connector="meteora", chain_network=NET, wallet_address=WALLET, base_token="SOL", quote_token="USDC", base_token_amount=1.0, - config_address="CFG123") + extra_params={"configAddress": "CFG123"}) c = calls[0] assert (c["method"], c["path"]) == ("POST", "trading/amm/create-pool") assert c["json"]["configAddress"] == "CFG123" - # Raydium/Uniswap extras and seed-price fields omitted when unset - for k in ("feeConfigIndex", "gasPrice", "maxGas", "quoteTokenAmount", "initialPrice"): + # Raydium extras, seeding slippage and seed-price fields omitted when unset + for k in ("ammConfigIndex", "slippagePct", "quoteTokenAmount", "initialPrice"): assert k not in c["json"] @pytest.mark.asyncio -async def test_amm_create_pool_raydium_fee_config_index(client_and_calls): +async def test_amm_create_pool_raydium_amm_config_index(client_and_calls): client, calls = client_and_calls await client.amm_create_pool(connector="raydium", chain_network=NET, wallet_address=WALLET, base_token="SOL", quote_token="USDC", base_token_amount=1.0, - fee_config_index=0, quote_token_amount=100.0) + extra_params={"ammConfigIndex": 0}, quote_token_amount=100.0) c = calls[0] - assert c["json"]["feeConfigIndex"] == 0 + assert c["json"]["ammConfigIndex"] == 0 assert c["json"]["quoteTokenAmount"] == 100.0 assert "configAddress" not in c["json"] @pytest.mark.asyncio -async def test_amm_create_pool_uniswap_gas_extras(client_and_calls): +async def test_amm_create_pool_uniswap_seeding_slippage(client_and_calls): + """EVM seeding slippage is the standard slippagePct field, not an extra param.""" client, calls = client_and_calls await client.amm_create_pool(connector="uniswap", chain_network="ethereum-mainnet", wallet_address=WALLET, base_token="WETH", quote_token="USDC", base_token_amount=1.0, - initial_price=3000.0, gas_price=20.0, max_gas=500000) + initial_price=3000.0, slippage_pct=0.5) c = calls[0] - assert c["json"]["gasPrice"] == 20.0 - assert c["json"]["maxGas"] == 500000 + assert c["json"]["slippagePct"] == 0.5 assert c["json"]["initialPrice"] == 3000.0 - assert "configAddress" not in c["json"] and "feeConfigIndex" not in c["json"] + assert "configAddress" not in c["json"] and "ammConfigIndex" not in c["json"] + + +@pytest.mark.asyncio +async def test_clmm_create_pool_meteora_extras(client_and_calls): + """CLMM create-pool extras ride extra_params under Gateway's names + (binStep/feeBps/ammConfigIndex — no gas keys, those don't exist on the route).""" + client, calls = client_and_calls + await client.clmm_create_pool(connector="meteora", chain_network=NET, wallet_address=WALLET, + base_token="SOL", quote_token="USDC", initial_price=100.0, + extra_params={"binStep": 20, "feeBps": 20}) + c = calls[0] + assert (c["method"], c["path"]) == ("POST", "trading/clmm/create-pool") + assert c["json"]["binStep"] == 20 + assert c["json"]["feeBps"] == 20 + assert c["json"]["initialPrice"] == 100.0 + assert "ammConfigIndex" not in c["json"] diff --git a/test/test_gateway_error_masking.py b/test/test_gateway_error_masking.py index 0a306428..15f1259d 100644 --- a/test/test_gateway_error_masking.py +++ b/test/test_gateway_error_masking.py @@ -109,7 +109,10 @@ async def test_refresh_does_not_close_position_on_gateway_error(): @pytest.mark.asyncio -async def test_refresh_closes_position_missing_from_valid_list(): +async def test_refresh_skips_position_missing_from_valid_list(): + """Absence from ONE positions-owned read is not proof of closure (RPC lag): + the refresh skips the update and leaves close detection to the poller's + consecutive-miss gate — a single read must never close a live position.""" from routers.gateway_clmm import _refresh_position_data accounts_service = _mock_accounts_service(clmm_positions_owned=[{"address": "OTHER"}]) @@ -120,7 +123,8 @@ async def test_refresh_closes_position_missing_from_valid_list(): ) await _refresh_position_data(_position(), accounts_service, clmm_repo) - clmm_repo.close_position.assert_awaited_once_with("POS") + clmm_repo.close_position.assert_not_awaited() + clmm_repo.update_position_liquidity.assert_not_awaited() # ============================================ @@ -162,3 +166,29 @@ async def test_poller_confirms_transaction(): poller = _poller_with_result({"txStatus": 1, "fee": 0.00001, "error": None}) result = await poller._check_transaction_status("solana", "mainnet-beta", "TX") assert result["status"] == "CONFIRMED" + # fee of exactly 0 must survive (no truthiness drop) + poller = _poller_with_result({"txStatus": 1, "fee": 0, "error": None}) + result = await poller._check_transaction_status("solana", "mainnet-beta", "TX") + assert result["gas_fee"] == 0 + + +@pytest.mark.asyncio +async def test_poller_pending_with_transient_error_stays_pending(): + """Gateway deliberately returns txStatus 0 WITH an error message for transient + poll failures ("poll again, don't give up") — the error field must never + promote a pending transaction to FAILED.""" + poller = _poller_with_result({ + "txStatus": 0, + "error": "Error polling transaction: 429 Too Many Requests", + }) + result = await poller._check_transaction_status("solana", "mainnet-beta", "TX") + assert result["status"] == "PENDING" + + +@pytest.mark.asyncio +async def test_poller_reports_not_found_as_dropped(): + """txStatus -2 is NOT_FOUND — terminal on Solana after blockhash expiry; the + caller applies the grace window, but the classifier must not call it pending.""" + poller = _poller_with_result({"txStatus": -2, "error": None}) + result = await poller._check_transaction_status("solana", "mainnet-beta", "TX") + assert result["status"] == "DROPPED"