diff --git a/environment_conda.yml b/environment_conda.yml index 793e779..2662b5c 100644 --- a/environment_conda.yml +++ b/environment_conda.yml @@ -22,5 +22,6 @@ dependencies: - pathlib - streamlit-ace - st-pages + - streamlit-elements==0.1.* - git+https://github.com/hummingbot/hbot-remote-client-py.git - git+https://github.com/hummingbot/docker-manager.git diff --git a/hummingbot_files/bot_configs/master_bot_conf/conf/.password_verification b/hummingbot_files/bot_configs/master_bot_conf/conf/.password_verification new file mode 100644 index 0000000..b8c7618 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/conf/.password_verification @@ -0,0 +1 @@ +7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20223864336365306436393461623131396334363135663935366464653839363063227d2c202263697068657274657874223a20223836333266323430613563306131623665353664222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226566373330376531636464373964376132303338323534656139343433663930227d2c20226d6163223a202266393439383534613530633138363633386363353962336133363665633962353333386633613964373266636635343066313034333361353431636232306438227d2c202276657273696f6e223a20337d \ No newline at end of file diff --git a/hummingbot_files/bot_configs/master_bot_conf/conf/conf_client.yml b/hummingbot_files/bot_configs/master_bot_conf/conf/conf_client.yml new file mode 100644 index 0000000..f6ec342 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/conf/conf_client.yml @@ -0,0 +1,199 @@ +#################################### +### client_config_map config ### +#################################### + +instance_id: 039758736d451914503a45ff596e168902d62557 + +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 + +telegram_mode: {} + +# 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 + +# Can store the previous strategy ran for quick retrieval. +previous_strategy: null + +# 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 + +pmm_script_mode: {} + +# Balance Limit Configurations +# e.g. Setting USDT and BTC limits on Binance. +# balance_asset_limit: +# binance: +# BTC: 0.1 +# USDT: 1000 +balance_asset_limit: + bybit_testnet: {} + lbank: {} + binance_us: {} + crypto_com: {} + ascend_ex_paper_trade: {} + hotbit: {} + gate_io_paper_trade: {} + bitmex_testnet: {} + ndax_testnet: {} + huobi: {} + probit_kr: {} + altmarkets: {} + hitbtc: {} + foxbit: {} + ascend_ex: {} + binance: {} + okx: {} + ciex: {} + bitmex: {} + bitfinex: {} + probit: {} + kraken: {} + kucoin: {} + bitmart: {} + bybit: {} + bittrex: {} + btc_markets: {} + mock_paper_exchange: {} + kucoin_paper_trade: {} + ndax: {} + loopring: {} + mexc: {} + whitebit: {} + coinbase_pro: {} + binance_paper_trade: {} + gate_io: {} + +# 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' + +certs_path: /home/hummingbot/certs + +# Whether to enable aggregated order and trade data collection +anonymized_metrics_mode: + anonymized_metrics_interval_min: 15.0 + +# Command Shortcuts +# Define abbreviations for often used commands +# or batch grouped commands together +command_shortcuts: +- command: spreads + help: Set bid and ask spread + arguments: + - Bid Spread + - Ask Spread + output: + - config bid_spread $1 + - config ask_spread $2 + +# A source for rate oracle, currently ascend_ex, binance, coin_gecko, kucoin, gate_io +rate_oracle_source: + name: binance + +# A universal token which to display tokens values in, e.g. USD,EUR,BTC +global_token: + global_token_name: USD + global_token_symbol: $ + +# 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 + USDT: 1000.0 + ONE: 1000.0 + USDQ: 1000.0 + TUSD: 1000.0 + ETH: 10.0 + WETH: 10.0 + USDC: 1000.0 + DAI: 1000.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: true + market_data_collection_interval: 60 + market_data_collection_depth: 20 diff --git a/hummingbot_files/bot_configs/master_bot_conf/conf/conf_fee_overrides.yml b/hummingbot_files/bot_configs/master_bot_conf/conf/conf_fee_overrides.yml new file mode 100644 index 0000000..76512b1 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/conf/conf_fee_overrides.yml @@ -0,0 +1,304 @@ +######################################## +### 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 data_downloader/conf_fee_override.yml +# will work. This file currently needs to be in sync with hummingbot list of +# supported exchanges +altmarkets_buy_percent_fee_deducted_from_returns: +altmarkets_maker_fixed_fees: +altmarkets_maker_percent_fee: +altmarkets_percent_fee_token: +altmarkets_taker_fixed_fees: +altmarkets_taker_percent_fee: +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: +bittrex_buy_percent_fee_deducted_from_returns: +bittrex_maker_fixed_fees: +bittrex_maker_percent_fee: +bittrex_percent_fee_token: +bittrex_taker_fixed_fees: +bittrex_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: +crypto_com_buy_percent_fee_deducted_from_returns: +crypto_com_maker_fixed_fees: +crypto_com_maker_percent_fee: +crypto_com_percent_fee_token: +crypto_com_taker_fixed_fees: +crypto_com_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: +loopring_buy_percent_fee_deducted_from_returns: +loopring_maker_fixed_fees: +loopring_maker_percent_fee: +loopring_percent_fee_token: +loopring_taker_fixed_fees: +loopring_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: +probit_buy_percent_fee_deducted_from_returns: +probit_kr_buy_percent_fee_deducted_from_returns: +probit_kr_maker_fixed_fees: +probit_kr_maker_percent_fee: +probit_kr_percent_fee_token: +probit_kr_taker_fixed_fees: +probit_kr_taker_percent_fee: +probit_maker_fixed_fees: +probit_maker_percent_fee: +probit_percent_fee_token: +probit_taker_fixed_fees: +probit_taker_percent_fee: +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: +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: +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: +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: +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: +whitebit_percent_fee_token: +whitebit_maker_percent_fee: +whitebit_taker_percent_fee: +whitebit_buy_percent_fee_deducted_from_returns: +whitebit_maker_fixed_fees: +whitebit_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: +ciex_percent_fee_token: +ciex_maker_percent_fee: +ciex_taker_percent_fee: +ciex_buy_percent_fee_deducted_from_returns: +ciex_maker_fixed_fees: +ciex_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: +lbank_percent_fee_token: +lbank_maker_percent_fee: +lbank_taker_percent_fee: +lbank_buy_percent_fee_deducted_from_returns: +lbank_maker_fixed_fees: +lbank_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: +hotbit_percent_fee_token: +hotbit_maker_percent_fee: +hotbit_taker_percent_fee: +hotbit_buy_percent_fee_deducted_from_returns: +hotbit_maker_fixed_fees: +hotbit_taker_fixed_fees: +btc_markets_maker_fixed_fees: +btc_markets_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: diff --git a/hummingbot_files/bot_configs/master_bot_conf/conf/hummingbot_logs.yml b/hummingbot_files/bot_configs/master_bot_conf/conf/hummingbot_logs.yml new file mode 100755 index 0000000..8e65271 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/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/hummingbot_files/bot_configs/master_bot_conf/conf_client.yml b/hummingbot_files/bot_configs/master_bot_conf/conf_client.yml new file mode 100644 index 0000000..fe5e177 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/conf_client.yml @@ -0,0 +1,194 @@ +#################################### +### client_config_map config ### +#################################### + +instance_id: e90c0d6f2b1e2d54fa0c0a69612b07174320963b + +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 + +telegram_mode: {} + +# MQTT Bridge configuration. +mqtt_bridge: + mqtt_host: localhost + mqtt_port: 1883 + mqtt_username: admin + mqtt_password: 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 + +# Can store the previous strategy ran for quick retrieval. +previous_strategy: null + +# 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 + +pmm_script_mode: {} + +# Balance Limit Configurations +# e.g. Setting USDT and BTC limits on Binance. +# balance_asset_limit: +# binance: +# BTC: 0.1 +# USDT: 1000 +balance_asset_limit: + kucoin: {} + ciex: {} + ascend_ex_paper_trade: {} + crypto_com: {} + mock_paper_exchange: {} + btc_markets: {} + bitmart: {} + hitbtc: {} + loopring: {} + mexc: {} + polkadex: {} + bybit: {} + foxbit: {} + gate_io_paper_trade: {} + kucoin_paper_trade: {} + altmarkets: {} + ascend_ex: {} + bittrex: {} + probit_kr: {} + binance: {} + bybit_testnet: {} + okx: {} + bitmex: {} + binance_us: {} + probit: {} + gate_io: {} + lbank: {} + whitebit: {} + bitmex_testnet: {} + kraken: {} + huobi: {} + binance_paper_trade: {} + ndax_testnet: {} + coinbase_pro: {} + ndax: {} + bitfinex: {} + +# 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' + +certs_path: /home/hummingbot/certs + +# Whether to enable aggregated order and trade data collection +anonymized_metrics_mode: + anonymized_metrics_interval_min: 15.0 + +# Command Shortcuts +# Define abbreviations for often used commands +# or batch grouped commands together +command_shortcuts: +- command: spreads + help: Set bid and ask spread + arguments: + - Bid Spread + - Ask Spread + output: + - config bid_spread $1 + - config ask_spread $2 + +# A source for rate oracle, currently ascend_ex, binance, coin_gecko, kucoin, gate_io +rate_oracle_source: + name: binance + +# A universal token which to display tokens values in, e.g. USD,EUR,BTC +global_token: + global_token_name: USD + global_token_symbol: $ + +# 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 + USDT: 1000.0 + ONE: 1000.0 + USDQ: 1000.0 + TUSD: 1000.0 + ETH: 10.0 + WETH: 10.0 + USDC: 1000.0 + DAI: 1000.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 diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/1overN_portfolio.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/1overN_portfolio.py new file mode 100644 index 0000000..487be00 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/1overN_portfolio.py @@ -0,0 +1,213 @@ +import decimal +import logging +import math +from decimal import Decimal +from typing import Dict + +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.core.data_type.common import OrderType +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase +from hummingbot.strategy.strategy_py_base import ( + BuyOrderCompletedEvent, + BuyOrderCreatedEvent, + MarketOrderFailureEvent, + OrderCancelledEvent, + OrderExpiredEvent, + OrderFilledEvent, + SellOrderCompletedEvent, + SellOrderCreatedEvent, +) + + +def create_differences_bar_chart(differences_dict): + diff_str = "Differences to 1/N:\n" + bar_length = 20 + for asset, deficit in differences_dict.items(): + deficit_percentage = deficit * 100 + filled_length = math.ceil(abs(deficit) * bar_length) + + if deficit > 0: + bar = f"{asset:6}: {' ' * bar_length}|{'#' * filled_length:<{bar_length}} +{deficit_percentage:.4f}%" + else: + bar = f"{asset:6}: {'#' * filled_length:>{bar_length}}|{' ' * bar_length} -{-deficit_percentage:.4f}%" + diff_str += bar + "\n" + return diff_str + + +class OneOverNPortfolio(ScriptStrategyBase): + """ + This strategy aims to create a 1/N cryptocurrency portfolio, providing perfect diversification without + parametrization and giving a reasonable baseline performance. + https://www.notion.so/1-N-Index-Portfolio-26752a174c5a4648885b8c344f3f1013 + Future improvements: + - add quote_currency balance as funding so that it can be traded, and it is not stuck when some trades are lost by + the exchange + - create a state machine so that all sells are executed before buy orders are submitted. Thus guaranteeing the + funding + """ + + exchange_name = "binance_paper_trade" + quote_currency = "USDT" + # top 10 coins by market cap, excluding stablecoins + base_currencies = ["BTC", "ETH", "MATIC", "XRP", "BNB", "ADA", "DOT", "LTC", "DOGE", "SOL"] + pairs = {f"{currency}-USDT" for currency in base_currencies} + + #: Define markets to instruct Hummingbot to create connectors on the exchanges and markets you need + markets = {exchange_name: pairs} + activeOrders = 0 + + def __init__(self, connectors: Dict[str, ConnectorBase]): + super().__init__(connectors) + self.total_available_balance = None + self.differences_dict = None + self.quote_balances = None + self.base_balances = None + + def on_tick(self): + #: check current balance of coins + balance_df = self.get_balance_df() + #: Filter by exchange "binance_paper_trade" + exchange_balance_df = balance_df.loc[balance_df["Exchange"] == self.exchange_name] + self.base_balances = self.calculate_base_balances(exchange_balance_df) + self.quote_balances = self.calculate_quote_balances(self.base_balances) + + #: Sum the available balances + self.total_available_balance = sum(balances[1] for balances in self.quote_balances.values()) + self.logger().info(f"TOT ({self.quote_currency}): {self.total_available_balance}") + self.logger().info( + f"TOT/{len(self.base_currencies)} ({self.quote_currency}): {self.total_available_balance / len(self.base_currencies)}") + #: Calculate the percentage of each available_balance over total_available_balance + total_available_balance = self.total_available_balance + percentages_dict = {} + for asset, balances in self.quote_balances.items(): + available_balance = balances[1] + percentage = (available_balance / total_available_balance) + percentages_dict[asset] = percentage + self.logger().info(f"Total share {asset}: {percentage * 100}%") + number_of_assets = Decimal(len(self.quote_balances)) + #: Calculate the difference between each percentage and 1/number_of_assets + differences_dict = self.calculate_deficit_percentages(number_of_assets, percentages_dict) + self.differences_dict = differences_dict + + # Calculate the absolute differences in quote currency + deficit_over_current_price = {} + for asset, deficit in differences_dict.items(): + current_price = self.quote_balances[asset][2] + deficit_over_current_price[asset] = deficit / current_price + #: Calculate the difference in pieces of each base asset + differences_in_base_asset = {} + for asset, deficit in deficit_over_current_price.items(): + differences_in_base_asset[asset] = deficit * total_available_balance + #: Create an ordered list of asset-deficit pairs starting from the smallest negative deficit ending with the + # biggest positive deficit + ordered_trades = sorted(differences_in_base_asset.items(), key=lambda x: x[1]) + #: log the planned ordered trades with sequence number + for i, (asset, deficit) in enumerate(ordered_trades): + trade_number = i + 1 + trade_type = "sell" if deficit < Decimal('0') else "buy" + self.logger().info(f"Trade {trade_number}: {trade_type} {asset}: {deficit}") + + if 0 < self.activeOrders: + self.logger().info(f"Wait to trade until all active orders have completed: {self.activeOrders}") + return + for i, (asset, deficit) in enumerate(ordered_trades): + quote_price = self.quote_balances[asset][2] + # We don't trade under 1 quote value, e.g. dollar. We can save trading fees by increasing this amount + if abs(deficit * quote_price) < 1: + self.logger().info(f"{abs(deficit * quote_price)} < 1 too small to trade") + continue + trade_is_buy = True if deficit > Decimal('0') else False + try: + if trade_is_buy: + self.buy(connector_name=self.exchange_name, trading_pair=f"{asset}-{self.quote_currency}", + amount=abs(deficit), order_type=OrderType.MARKET, price=quote_price) + else: + self.sell(connector_name=self.exchange_name, trading_pair=f"{asset}-{self.quote_currency}", + amount=abs(deficit), order_type=OrderType.MARKET, price=quote_price) + except decimal.InvalidOperation as e: + # Handle the error by logging it or taking other appropriate actions + print(f"Caught an error: {e}") + self.activeOrders -= 1 + + return + + def calculate_deficit_percentages(self, number_of_assets, percentages_dict): + differences_dict = {} + for asset, percentage in percentages_dict.items(): + deficit = (Decimal('1') / number_of_assets) - percentage + differences_dict[asset] = deficit + self.logger().info(f"Missing from 1/N {asset}: {deficit * 100}%") + return differences_dict + + def calculate_quote_balances(self, base_balances): + #: Multiply each balance with the current price to get the balances in the quote currency + quote_balances = {} + connector = self.connectors[self.exchange_name] + for asset, balances in base_balances.items(): + trading_pair = f"{asset}-{self.quote_currency}" + # noinspection PyUnresolvedReferences + current_price = Decimal(connector.get_mid_price(trading_pair)) + total_balance = balances[0] * current_price + available_balance = balances[1] * current_price + quote_balances[asset] = (total_balance, available_balance, current_price) + self.logger().info( + f"{asset} * {current_price} {self.quote_currency} = {available_balance} {self.quote_currency}") + return quote_balances + + def calculate_base_balances(self, exchange_balance_df): + base_balances = {} + for _, row in exchange_balance_df.iterrows(): + asset_name = row["Asset"] + if asset_name in self.base_currencies: + total_balance = Decimal(row["Total Balance"]) + available_balance = Decimal(row["Available Balance"]) + base_balances[asset_name] = (total_balance, available_balance) + logging.info(f"{available_balance:015,.5f} {asset_name} \n") + return base_balances + + def format_status(self) -> str: + # checking if last member variable in on_tick is set, so we can start + if self.differences_dict is None: + return "SYSTEM NOT READY... booting" + # create a table of base_balances and quote_balances and the summed up total of the quote_balances + table_of_balances = "base balances quote balances price\n" + for asset_name, base_balances in self.base_balances.items(): + quote_balance = self.quote_balances[asset_name][1] + price = self.quote_balances[asset_name][2] + table_of_balances += f"{base_balances[1]:15,.5f} {asset_name:5} {quote_balance:15,.5f} {price:15,.5f} {self.quote_currency}\n" + table_of_balances += f"TOT ({self.quote_currency}): {self.total_available_balance:15,.2f}\n" + table_of_balances += f"TOT/{len(self.base_currencies)} ({self.quote_currency}): {self.total_available_balance / len(self.base_currencies):15,.2f}\n" + return f"active orders: {self.activeOrders}\n" + \ + table_of_balances + "\n" + \ + create_differences_bar_chart(self.differences_dict) + + def did_create_buy_order(self, event: BuyOrderCreatedEvent): + self.activeOrders += 1 + logging.info(f"Created Buy - Active Orders ++: {self.activeOrders}") + + def did_create_sell_order(self, event: SellOrderCreatedEvent): + self.activeOrders += 1 + logging.info(f"Created Sell - Active Orders ++: {self.activeOrders}") + + def did_complete_buy_order(self, event: BuyOrderCompletedEvent): + self.activeOrders -= 1 + logging.info(f"Completed Buy - Active Orders --: {self.activeOrders}") + + def did_complete_sell_order(self, event: SellOrderCompletedEvent): + self.activeOrders -= 1 + logging.info(f"Completed Sell - Active Orders --: {self.activeOrders}") + + def did_cancel_order(self, event: OrderCancelledEvent): + self.activeOrders -= 1 + logging.info(f"Canceled Order - Active Order --: {self.activeOrders}") + + def did_expire_order(self, event: OrderExpiredEvent): + self.activeOrders -= 1 + logging.info(f"Expired Order - Active Order --: {self.activeOrders}") + + def did_fail_order(self, event: MarketOrderFailureEvent): + self.activeOrders -= 1 + logging.info(f"Failed Order - Active Order --: {self.activeOrders}") + + def did_fill_order(self, event: OrderFilledEvent): + logging.info(f"Filled Order - Active Order ??: {self.activeOrders}") diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/adjusted_mid_price.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/adjusted_mid_price.py new file mode 100644 index 0000000..154f995 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/adjusted_mid_price.py @@ -0,0 +1,147 @@ +from decimal import Decimal +from typing import List + +from hummingbot.connector.exchange_base import ExchangeBase +from hummingbot.core.data_type.common import OrderType, TradeType +from hummingbot.core.data_type.order_candidate import OrderCandidate +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class AdjustedMidPrice(ScriptStrategyBase): + """ + BotCamp Cohort: Sept 2022 + Design Template: https://hummingbot-foundation.notion.site/PMM-with-Adjusted-Midpoint-4259e7aef7bf403dbed35d1ed90f36fe + Video: - + Description: + This is an example of a pure market making strategy with an adjusted mid price. The mid price is adjusted to + the midpoint of a hypothetical buy and sell of a user defined {test_volume}. + Example: + let test_volume = 10 and the pair = BTC-USDT, then the new mid price will be the mid price of the following two points: + 1) the average fill price of a hypothetical market buy of 10 BTC + 2) the average fill price of a hypothetical market sell of 10 BTC + """ + + # The following strategy dictionary are parameters that the script operator can adjustS + strategy = { + "test_volume": 50, # the amount in base currancy to make the hypothetical market buy and market sell. + "bid_spread": .1, # how far away from the mid price do you want to place the first bid order (1 indicated 1%) + "ask_spread": .1, # how far away from the mid price do you want to place the first bid order (1 indicated 1%) + "amount": .1, # the amount in base currancy you want to buy or sell + "order_refresh_time": 60, + "market": "binance_paper_trade", + "pair": "BTC-USDT" + } + + markets = {strategy["market"]: {strategy["pair"]}} + + @property + def connector(self) -> ExchangeBase: + return self.connectors[self.strategy["market"]] + + def on_tick(self): + """ + Runs every tick_size seconds, this is the main operation of the strategy. + This method does two things: + - Refreshes the current bid and ask if they are set to None + - Cancels the current bid or current ask if they are past their order_refresh_time + The canceled orders will be refreshed next tic + """ + ## + # refresh order logic + ## + active_orders = self.get_active_orders(self.strategy["market"]) + # determine if we have an active bid and ask. We will only ever have 1 bid and 1 ask, so this logic would not work in the case of hanging orders + active_bid = None + active_ask = None + for order in active_orders: + if order.is_buy: + active_bid = order + else: + active_ask = order + proposal: List(OrderCandidate) = [] + if active_bid is None: + proposal.append(self.create_order(True)) + if active_ask is None: + proposal.append(self.create_order(False)) + if (len(proposal) > 0): + # we have proposed orders to place + # the next line will set the amount to 0 if we do not have the budget for the order and will quantize the amount if we have the budget + adjusted_proposal: List(OrderCandidate) = self.connector.budget_checker.adjust_candidates(proposal, all_or_none=True) + # we will set insufficient funds to true if any of the orders were set to zero + insufficient_funds = False + for order in adjusted_proposal: + if (order.amount == 0): + insufficient_funds = True + # do not place any orders if we have any insufficient funds and notify user + if (insufficient_funds): + self.logger().info("Insufficient funds. No more orders will be placed") + else: + # place orders + for order in adjusted_proposal: + if order.order_side == TradeType.BUY: + self.buy(self.strategy["market"], order.trading_pair, Decimal(self.strategy['amount']), order.order_type, Decimal(order.price)) + elif order.order_side == TradeType.SELL: + self.sell(self.strategy["market"], order.trading_pair, Decimal(self.strategy['amount']), order.order_type, Decimal(order.price)) + ## + # cancel order logic + # (canceled orders will be refreshed next tick) + ## + for order in active_orders: + if (order.age() > self.strategy["order_refresh_time"]): + self.cancel(self.strategy["market"], self.strategy["pair"], order.client_order_id) + + def create_order(self, is_bid: bool) -> OrderCandidate: + """ + Create a propsal for the current bid or ask using the adjusted mid price. + """ + mid_price = Decimal(self.adjusted_mid_price()) + bid_spread = Decimal(self.strategy["bid_spread"]) + ask_spread = Decimal(self.strategy["ask_spread"]) + bid_price = mid_price - mid_price * bid_spread * Decimal(.01) + ask_price = mid_price + mid_price * ask_spread * Decimal(.01) + price = bid_price if is_bid else ask_price + price = self.connector.quantize_order_price(self.strategy["pair"], Decimal(price)) + order = OrderCandidate( + trading_pair=self.strategy["pair"], + is_maker=False, + order_type=OrderType.LIMIT, + order_side=TradeType.BUY if is_bid else TradeType.SELL, + amount=Decimal(self.strategy["amount"]), + price=price) + return order + + def adjusted_mid_price(self): + """ + Returns the price of a hypothetical buy and sell or the base asset where the amout is {strategy.test_volume} + """ + ask_result = self.connector.get_quote_volume_for_base_amount(self.strategy["pair"], True, self.strategy["test_volume"]) + bid_result = self.connector.get_quote_volume_for_base_amount(self.strategy["pair"], False, self.strategy["test_volume"]) + average_ask = ask_result.result_volume / ask_result.query_volume + average_bid = bid_result = bid_result.result_volume / bid_result.query_volume + return average_bid + ((average_ask - average_bid) / 2) + + def format_status(self) -> str: + """ + Returns status of the current strategy on user balances and current active orders. This function is called + when status command is issued. Override this function to create custom status display output. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + warning_lines = [] + warning_lines.extend(self.network_warning(self.get_market_trading_pair_tuples())) + actual_mid_price = self.connector.get_mid_price(self.strategy["pair"]) + adjusted_mid_price = self.adjusted_mid_price() + lines.extend(["", " Adjusted mid price: " + str(adjusted_mid_price)] + [" Actual mid price: " + str(actual_mid_price)]) + balance_df = self.get_balance_df() + lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")]) + try: + df = self.active_orders_df() + lines.extend(["", " Orders:"] + [" " + line for line in df.to_string(index=False).split("\n")]) + except ValueError: + lines.extend(["", " No active maker orders."]) + + warning_lines.extend(self.balance_warning(self.get_market_trading_pair_tuples())) + if len(warning_lines) > 0: + lines.extend(["", "*** WARNINGS ***"] + warning_lines) + return "\n".join(lines) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/amm_data_feed_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/amm_data_feed_example.py new file mode 100644 index 0000000..ee35cec --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/amm_data_feed_example.py @@ -0,0 +1,48 @@ +from decimal import Decimal +from typing import Dict + +import pandas as pd + +from hummingbot.client.ui.interface_utils import format_df_for_printout +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.data_feed.amm_gateway_data_feed import AmmGatewayDataFeed +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class AMMDataFeedExample(ScriptStrategyBase): + amm_data_feed_uniswap = AmmGatewayDataFeed( + connector_chain_network="uniswap_polygon_mainnet", + trading_pairs={"LINK-USDC", "AAVE-USDC", "WMATIC-USDT"}, + order_amount_in_base=Decimal("1"), + ) + amm_data_feed_quickswap = AmmGatewayDataFeed( + connector_chain_network="quickswap_polygon_mainnet", + trading_pairs={"LINK-USDC", "AAVE-USDC", "WMATIC-USDT"}, + order_amount_in_base=Decimal("1"), + ) + markets = {"binance_paper_trade": {"BTC-USDT"}} + + def __init__(self, connectors: Dict[str, ConnectorBase]): + super().__init__(connectors) + self.amm_data_feed_uniswap.start() + self.amm_data_feed_quickswap.start() + + def on_stop(self): + self.amm_data_feed_uniswap.stop() + self.amm_data_feed_quickswap.stop() + + def on_tick(self): + pass + + def format_status(self) -> str: + if self.amm_data_feed_uniswap.is_ready() and self.amm_data_feed_quickswap.is_ready(): + lines = [] + rows = [] + rows.extend(dict(price) for token, price in self.amm_data_feed_uniswap.price_dict.items()) + rows.extend(dict(price) for token, price in self.amm_data_feed_quickswap.price_dict.items()) + df = pd.DataFrame(rows) + prices_str = format_df_for_printout(df, table_format="psql") + lines.append(f"AMM Data Feed is ready.\n{prices_str}") + return "\n".join(lines) + else: + return "AMM Data Feed is not ready." diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/amm_price_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/amm_price_example.py new file mode 100644 index 0000000..8abb750 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/amm_price_example.py @@ -0,0 +1,49 @@ +from hummingbot.core.event.events import TradeType +from hummingbot.core.gateway.gateway_http_client import GatewayHttpClient +from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.strategy.script_strategy_base import Decimal, ScriptStrategyBase + + +class AmmPriceExample(ScriptStrategyBase): + """ + This example shows how to call the /amm/price Gateway endpoint to fetch price for a swap + """ + # swap params + connector_chain_network = "uniswap_ethereum_goerli" + trading_pair = {"WETH-DAI"} + side = "SELL" + order_amount = Decimal("0.01") + markets = { + connector_chain_network: trading_pair + } + on_going_task = False + + def on_tick(self): + # only execute once + if not self.on_going_task: + self.on_going_task = True + # wrap async task in safe_ensure_future + safe_ensure_future(self.async_task()) + + # async task since we are using Gateway + async def async_task(self): + base, quote = list(self.trading_pair)[0].split("-") + connector, chain, network = self.connector_chain_network.split("_") + if (self.side == "BUY"): + trade_type = TradeType.BUY + else: + trade_type = TradeType.SELL + + # fetch price + self.logger().info(f"POST /amm/price [ connector: {connector}, base: {base}, quote: {quote}, amount: {self.order_amount}, side: {self.side} ]") + data = await GatewayHttpClient.get_instance().get_price( + chain, + network, + connector, + base, + quote, + self.order_amount, + trade_type + ) + self.logger().info(f"Price: {data['price']}") + self.logger().info(f"Amount: {data['amount']}") diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/amm_trade_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/amm_trade_example.py new file mode 100644 index 0000000..e123fd8 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/amm_trade_example.py @@ -0,0 +1,120 @@ +import asyncio + +from hummingbot.client.settings import GatewayConnectionSetting +from hummingbot.core.event.events import TradeType +from hummingbot.core.gateway.gateway_http_client import GatewayHttpClient +from hummingbot.core.utils.async_utils import safe_ensure_future +from hummingbot.strategy.script_strategy_base import Decimal, ScriptStrategyBase + + +class AmmTradeExample(ScriptStrategyBase): + """ + This example shows how to call the /amm/trade Gateway endpoint to execute a swap transaction + """ + # swap params + connector_chain_network = "uniswap_ethereum_goerli" + trading_pair = {"WETH-DAI"} + side = "SELL" + order_amount = Decimal("0.01") + slippage_buffer = 0.01 + markets = { + connector_chain_network: trading_pair + } + on_going_task = False + + def on_tick(self): + # only execute once + if not self.on_going_task: + self.on_going_task = True + # wrap async task in safe_ensure_future + safe_ensure_future(self.async_task()) + + # async task since we are using Gateway + async def async_task(self): + base, quote = list(self.trading_pair)[0].split("-") + connector, chain, network = self.connector_chain_network.split("_") + if (self.side == "BUY"): + trade_type = TradeType.BUY + else: + trade_type = TradeType.SELL + + # fetch current price + self.logger().info(f"POST /amm/price [ connector: {connector}, base: {base}, quote: {quote}, amount: {self.order_amount}, side: {self.side} ]") + priceData = await GatewayHttpClient.get_instance().get_price( + chain, + network, + connector, + base, + quote, + self.order_amount, + trade_type + ) + self.logger().info(f"Price: {priceData['price']}") + self.logger().info(f"Amount: {priceData['amount']}") + + # add slippage buffer to current price + if (self.side == "BUY"): + price = float(priceData['price']) * (1 + self.slippage_buffer) + else: + price = float(priceData['price']) * (1 - self.slippage_buffer) + self.logger().info(f"Swap Limit Price: {price}") + + # fetch wallet address and print balances + gateway_connections_conf = GatewayConnectionSetting.load() + if len(gateway_connections_conf) < 1: + self.notify("No existing wallet.\n") + return + wallet = [w for w in gateway_connections_conf if w["chain"] == chain and w["connector"] == connector and w["network"] == network] + address = wallet[0]['wallet_address'] + await self.get_balance(chain, network, address, base, quote) + + # execute swap + self.logger().info(f"POST /amm/trade [ connector: {connector}, base: {base}, quote: {quote}, amount: {self.order_amount}, side: {self.side}, price: {price} ]") + tradeData = await GatewayHttpClient.get_instance().amm_trade( + chain, + network, + connector, + address, + base, + quote, + trade_type, + self.order_amount, + Decimal(price) + ) + + # poll for swap result and print resulting balances + await self.poll_transaction(chain, network, tradeData['txHash']) + await self.get_balance(chain, network, address, base, quote) + + # fetch and print balance of base and quote tokens + async def get_balance(self, chain, network, address, base, quote): + self.logger().info(f"POST /network/balance [ address: {address}, base: {base}, quote: {quote} ]") + balanceData = await GatewayHttpClient.get_instance().get_balances( + chain, + network, + address, + [base, quote] + ) + self.logger().info(f"Balances for {address}: {balanceData['balances']}") + + # continuously poll for transaction until confirmed + async def poll_transaction(self, chain, network, txHash): + pending: bool = True + while pending is True: + self.logger().info(f"POST /network/poll [ txHash: {txHash} ]") + pollData = await GatewayHttpClient.get_instance().get_transaction_status( + chain, + network, + txHash + ) + transaction_status = pollData.get("txStatus") + if transaction_status == 1: + self.logger().info(f"Trade with transaction hash {txHash} has been executed successfully.") + pending = False + elif transaction_status in [-1, 0, 2]: + self.logger().info(f"Trade is pending confirmation, Transaction hash: {txHash}") + await asyncio.sleep(2) + else: + self.logger().info(f"Unknown txStatus: {transaction_status}") + self.logger().info(f"{pollData}") + pending = False diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/backtest_mm_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/backtest_mm_example.py new file mode 100644 index 0000000..7218b83 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/backtest_mm_example.py @@ -0,0 +1,181 @@ +import logging +from datetime import datetime + +import numpy as np +import pandas as pd + +from hummingbot import data_path +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class BacktestMM(ScriptStrategyBase): + """ + BotCamp Cohort: 4 + Design Template: https://www.notion.so/hummingbot-foundation/Backtestable-Market-Making-Stategy-95c0d17e4042485bb90b7b2914af7f68?pvs=4 + Video: https://www.loom.com/share/e18380429e9443ceb1ef86eb131c14a2 + Description: This bot implements a simpler backtester for a market making strategy using the Binance candles feed. + After processing the user-defined backtesting parameters through historical OHLCV candles, it calculates a summary + table displayed in 'status' and saves the data to a CSV file. + + You may need to run 'balance paper [asset] [amount]' beforehand to set the initial balances used for backtesting. + """ + + # User-defined parameters + exchange = "binance" + trading_pair = "ETH-USDT" + order_amount = 0.1 + bid_spread_bps = 10 + ask_spread_bps = 10 + fee_bps = 10 + days = 7 + paper_trade_enabled = True + + # System parameters + precision = 2 + base, quote = trading_pair.split("-") + execution_exchange = f"{exchange}_paper_trade" if paper_trade_enabled else exchange + interval = "1m" + results_df = None + candle = CandlesFactory.get_candle(connector=exchange, trading_pair=trading_pair, interval=interval, max_records=days * 60 * 24) + candle.start() + + csv_path = data_path() + f"/backtest_{trading_pair}_{bid_spread_bps}_bid_{ask_spread_bps}_ask.csv" + markets = {f"{execution_exchange}": {trading_pair}} + + def on_tick(self): + if not self.candle.is_ready: + self.logger().info(f"Candles not ready yet for {self.trading_pair}! Missing {self.candle._candles.maxlen - len(self.candle._candles)}") + pass + else: + df = self.candle.candles_df + df['ask_price'] = df["open"] * (1 + self.ask_spread_bps / 10000) + df['bid_price'] = df["open"] * (1 - self.bid_spread_bps / 10000) + df['buy_amount'] = df['low'].le(df['bid_price']) * self.order_amount + df['sell_amount'] = df['high'].ge(df['ask_price']) * self.order_amount + df['fees_paid'] = (df['buy_amount'] * df['bid_price'] + df['sell_amount'] * df['ask_price']) * self.fee_bps / 10000 + df['base_delta'] = df['buy_amount'] - df['sell_amount'] + df['quote_delta'] = df['sell_amount'] * df['ask_price'] - df['buy_amount'] * df['bid_price'] - df['fees_paid'] + + if self.candle.is_ready and self.results_df is None: + df.to_csv(self.csv_path, index=False) + self.results_df = df + msg = "Backtesting complete - run 'status' to see results." + self.log_with_clock(logging.INFO, msg) + self.notify_hb_app_with_timestamp(msg) + + def on_stop(self): + self.candle.stop() + + def get_trades_df(self, df): + total_buy_trades = df['buy_amount'].ne(0).sum() + total_sell_trades = df['sell_amount'].ne(0).sum() + amount_bought = df['buy_amount'].sum() + amount_sold = df['sell_amount'].sum() + end_price = df.tail(1)['close'].values[0] + amount_bought_quote = amount_bought * end_price + amount_sold_quote = amount_sold * end_price + avg_buy_price = np.dot(df['bid_price'], df['buy_amount']) / amount_bought + avg_sell_price = np.dot(df['ask_price'], df['sell_amount']) / amount_sold + avg_total_price = (avg_buy_price * amount_bought + avg_sell_price * amount_sold) / (amount_bought + amount_sold) + + trades_columns = ["", "buy", "sell", "total"] + trades_data = [ + [f"{'Number of trades':<27}", total_buy_trades, total_sell_trades, total_buy_trades + total_sell_trades], + [f"{f'Total trade volume ({self.base})':<27}", + round(amount_bought, self.precision), + round(amount_sold, self.precision), + round(amount_bought + amount_sold, self.precision)], + [f"{f'Total trade volume ({self.quote})':<27}", + round(amount_bought_quote, self.precision), + round(amount_sold_quote, self.precision), + round(amount_bought_quote + amount_sold_quote, self.precision)], + [f"{'Avg price':<27}", + round(avg_buy_price, self.precision), + round(avg_sell_price, self.precision), + round(avg_total_price, self.precision)], + ] + return pd.DataFrame(data=trades_data, columns=trades_columns) + + def get_assets_df(self, df): + for connector_name, connector in self.connectors.items(): + base_bal_start = float(connector.get_balance(self.base)) + quote_bal_start = float(connector.get_balance(self.quote)) + base_bal_change = df['base_delta'].sum() + quote_bal_change = df['quote_delta'].sum() + base_bal_end = base_bal_start + base_bal_change + quote_bal_end = quote_bal_start + quote_bal_change + start_price = df.head(1)['open'].values[0] + end_price = df.tail(1)['close'].values[0] + base_bal_start_pct = base_bal_start / (base_bal_start + quote_bal_start / start_price) + base_bal_end_pct = base_bal_end / (base_bal_end + quote_bal_end / end_price) + + assets_columns = ["", "start", "end", "change"] + assets_data = [ + [f"{f'{self.base}':<27}", f"{base_bal_start:2}", round(base_bal_end, self.precision), round(base_bal_change, self.precision)], + [f"{f'{self.quote}':<27}", f"{quote_bal_start:2}", round(quote_bal_end, self.precision), round(quote_bal_change, self.precision)], + [f"{f'{self.base}-{self.quote} price':<27}", start_price, end_price, end_price - start_price], + [f"{'Base asset %':<27}", f"{base_bal_start_pct:.2%}", + f"{base_bal_end_pct:.2%}", + f"{base_bal_end_pct - base_bal_start_pct:.2%}"], + ] + return pd.DataFrame(data=assets_data, columns=assets_columns) + + def get_performance_df(self, df): + for connector_name, connector in self.connectors.items(): + base_bal_start = float(connector.get_balance(self.base)) + quote_bal_start = float(connector.get_balance(self.quote)) + base_bal_change = df['base_delta'].sum() + quote_bal_change = df['quote_delta'].sum() + start_price = df.head(1)['open'].values[0] + end_price = df.tail(1)['close'].values[0] + base_bal_end = base_bal_start + base_bal_change + quote_bal_end = quote_bal_start + quote_bal_change + hold_value = base_bal_end * start_price + quote_bal_end + current_value = base_bal_end * end_price + quote_bal_end + total_pnl = current_value - hold_value + fees_paid = df['fees_paid'].sum() + return_pct = total_pnl / hold_value + perf_data = [ + ["Hold portfolio value ", f"{round(hold_value, self.precision)} {self.quote}"], + ["Current portfolio value ", f"{round(current_value, self.precision)} {self.quote}"], + ["Trade P&L ", f"{round(total_pnl + fees_paid, self.precision)} {self.quote}"], + ["Fees paid ", f"{round(fees_paid, self.precision)} {self.quote}"], + ["Total P&L ", f"{round(total_pnl, self.precision)} {self.quote}"], + ["Return % ", f"{return_pct:2%} {self.quote}"], + ] + return pd.DataFrame(data=perf_data) + + def format_status(self) -> str: + if not self.ready_to_trade: + return "Market connectors are not ready." + if not self.candle.is_ready: + return (f"Candles not ready yet for {self.trading_pair}! Missing {self.candle._candles.maxlen - len(self.candle._candles)}") + + df = self.results_df + base, quote = self.trading_pair.split("-") + lines = [] + start_time = datetime.fromtimestamp(int(df.head(1)['timestamp'].values[0] / 1000)) + end_time = datetime.fromtimestamp(int(df.tail(1)['timestamp'].values[0] / 1000)) + + lines.extend( + [f"\n Start Time: {start_time.strftime('%Y-%m-%d %H:%M:%S')}"] + + [f" End Time: {end_time.strftime('%Y-%m-%d %H:%M:%S')}"] + + [f" Duration: {pd.Timedelta(seconds=(end_time - start_time).seconds)}"] + ) + lines.extend( + [f"\n Market: {self.exchange} / {self.trading_pair}"] + + [f" Spread(bps): {self.bid_spread_bps} bid / {self.ask_spread_bps} ask"] + + [f" Order Amount: {self.order_amount} {base}"] + ) + + trades_df = self.get_trades_df(df) + lines.extend(["", " Trades:"] + [" " + line for line in trades_df.to_string(index=False).split("\n")]) + + assets_df = self.get_assets_df(df) + lines.extend(["", " Assets:"] + [" " + line for line in assets_df.to_string(index=False).split("\n")]) + + performance_df = self.get_performance_df(df) + lines.extend(["", " Performance:"] + [" " + line for line in performance_df.to_string(index=False, header=False).split("\n")]) + + return "\n".join(lines) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/batch_order_update.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/batch_order_update.py new file mode 100644 index 0000000..531c07d --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/batch_order_update.py @@ -0,0 +1,122 @@ +from collections import defaultdict +from decimal import Decimal +from typing import List + +from hummingbot.connector.utils import combine_to_hb_trading_pair +from hummingbot.core.data_type.limit_order import LimitOrder +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + +CONNECTOR = "dexalot_avalanche_dexalot" +BASE = "AVAX" +QUOTE = "USDC" +TRADING_PAIR = combine_to_hb_trading_pair(base=BASE, quote=QUOTE) +AMOUNT = Decimal("0.5") +ORDERS_INTERVAL = 20 +PRICE_OFFSET_RATIO = Decimal("0.1") # 10% + + +class BatchOrderUpdate(ScriptStrategyBase): + markets = {CONNECTOR: {TRADING_PAIR}} + pingpong = 0 + + script_phase = 0 + + def on_tick(self): + if self.script_phase == 0: + self.place_two_orders_successfully() + elif self.script_phase == ORDERS_INTERVAL: + self.cancel_orders() + elif self.script_phase == ORDERS_INTERVAL * 2: + self.place_two_orders_with_one_zero_amount_that_will_fail() + elif self.script_phase == ORDERS_INTERVAL * 3: + self.cancel_orders() + self.script_phase += 1 + + def place_two_orders_successfully(self): + price = self.connectors[CONNECTOR].get_price(trading_pair=TRADING_PAIR, is_buy=True) + orders_to_create = [ + LimitOrder( + client_order_id="", + trading_pair=TRADING_PAIR, + is_buy=True, + base_currency=BASE, + quote_currency=QUOTE, + price=price * (1 - PRICE_OFFSET_RATIO), + quantity=AMOUNT, + ), + LimitOrder( + client_order_id="", + trading_pair=TRADING_PAIR, + is_buy=False, + base_currency=BASE, + quote_currency=QUOTE, + price=price * (1 + PRICE_OFFSET_RATIO), + quantity=AMOUNT, + ), + ] + + market_pair = self._market_trading_pair_tuple(connector_name=CONNECTOR, trading_pair=TRADING_PAIR) + market = market_pair.market + + submitted_orders: List[LimitOrder] = market.batch_order_create( + orders_to_create=orders_to_create, + ) + + for order in submitted_orders: + self.start_tracking_limit_order( + market_pair=market_pair, + order_id=order.client_order_id, + is_buy=order.is_buy, + price=order.price, + quantity=order.quantity, + ) + + def cancel_orders(self): + exchanges_to_orders = defaultdict(lambda: []) + exchanges_dict = {} + + for exchange, order in self.order_tracker.active_limit_orders: + exchanges_to_orders[exchange.name].append(order) + exchanges_dict[exchange.name] = exchange + + for exchange_name, orders_to_cancel in exchanges_to_orders.items(): + exchanges_dict[exchange_name].batch_order_cancel(orders_to_cancel=orders_to_cancel) + + def place_two_orders_with_one_zero_amount_that_will_fail(self): + price = self.connectors[CONNECTOR].get_price(trading_pair=TRADING_PAIR, is_buy=True) + orders_to_create = [ + LimitOrder( + client_order_id="", + trading_pair=TRADING_PAIR, + is_buy=True, + base_currency=BASE, + quote_currency=QUOTE, + price=price * (1 - PRICE_OFFSET_RATIO), + quantity=AMOUNT, + ), + LimitOrder( + client_order_id="", + trading_pair=TRADING_PAIR, + is_buy=False, + base_currency=BASE, + quote_currency=QUOTE, + price=price * (1 + PRICE_OFFSET_RATIO), + quantity=Decimal("0"), + ), + ] + + market_pair = self._market_trading_pair_tuple(connector_name=CONNECTOR, trading_pair=TRADING_PAIR) + market = market_pair.market + + submitted_orders: List[LimitOrder] = market.batch_order_create( + orders_to_create=orders_to_create, + ) + + for order in submitted_orders: + self.start_tracking_limit_order( + market_pair=market_pair, + order_id=order.client_order_id, + is_buy=order.is_buy, + price=order.price, + quantity=order.quantity, + ) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/batch_order_update_market_orders.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/batch_order_update_market_orders.py new file mode 100644 index 0000000..66696ed --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/batch_order_update_market_orders.py @@ -0,0 +1,104 @@ +import time +from decimal import Decimal +from typing import List + +from hummingbot.connector.utils import combine_to_hb_trading_pair +from hummingbot.core.data_type.limit_order import LimitOrder +from hummingbot.core.data_type.market_order import MarketOrder +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + +CONNECTOR = "bybit" +BASE = "ETH" +QUOTE = "BTC" +TRADING_PAIR = combine_to_hb_trading_pair(base=BASE, quote=QUOTE) +AMOUNT = Decimal("0.003") +ORDERS_INTERVAL = 20 +PRICE_OFFSET_RATIO = Decimal("0.1") # 10% + + +class BatchOrderUpdate(ScriptStrategyBase): + markets = {CONNECTOR: {TRADING_PAIR}} + pingpong = 0 + + script_phase = 0 + + def on_tick(self): + if self.script_phase == 0: + self.place_two_orders_successfully() + elif self.script_phase == ORDERS_INTERVAL: + self.place_two_orders_with_one_zero_amount_that_will_fail() + self.script_phase += 1 + + def place_two_orders_successfully(self): + orders_to_create = [ + MarketOrder( + order_id="", + trading_pair=TRADING_PAIR, + is_buy=True, + base_asset=BASE, + quote_asset=QUOTE, + amount=AMOUNT, + timestamp=time.time(), + ), + MarketOrder( + order_id="", + trading_pair=TRADING_PAIR, + is_buy=False, + base_asset=BASE, + quote_asset=QUOTE, + amount=AMOUNT, + timestamp=time.time(), + ), + ] + + market_pair = self._market_trading_pair_tuple(connector_name=CONNECTOR, trading_pair=TRADING_PAIR) + market = market_pair.market + + submitted_orders: List[LimitOrder, MarketOrder] = market.batch_order_create( + orders_to_create=orders_to_create, + ) + + for order in submitted_orders: + self.start_tracking_market_order( + market_pair=market_pair, + order_id=order.order_id, + is_buy=order.is_buy, + quantity=order.amount, + ) + + def place_two_orders_with_one_zero_amount_that_will_fail(self): + orders_to_create = [ + MarketOrder( + order_id="", + trading_pair=TRADING_PAIR, + is_buy=True, + base_asset=BASE, + quote_asset=QUOTE, + amount=AMOUNT, + timestamp=time.time(), + ), + MarketOrder( + order_id="", + trading_pair=TRADING_PAIR, + is_buy=True, + base_asset=BASE, + quote_asset=QUOTE, + amount=Decimal("0"), + timestamp=time.time(), + ), + ] + + market_pair = self._market_trading_pair_tuple(connector_name=CONNECTOR, trading_pair=TRADING_PAIR) + market = market_pair.market + + submitted_orders: List[LimitOrder, MarketOrder] = market.batch_order_create( + orders_to_create=orders_to_create, + ) + + for order in submitted_orders: + self.start_tracking_market_order( + market_pair=market_pair, + order_id=order.order_id, + is_buy=order.is_buy, + quantity=order.amount, + ) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/buy_dip_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/buy_dip_example.py new file mode 100644 index 0000000..399a59f --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/buy_dip_example.py @@ -0,0 +1,130 @@ +import logging +import time +from decimal import Decimal +from statistics import mean +from typing import List + +import requests + +from hummingbot.connector.exchange_base import ExchangeBase +from hummingbot.connector.utils import split_hb_trading_pair +from hummingbot.core.data_type.order_candidate import OrderCandidate +from hummingbot.core.event.events import OrderFilledEvent, OrderType, TradeType +from hummingbot.core.rate_oracle.rate_oracle import RateOracle +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class BuyDipExample(ScriptStrategyBase): + """ + THis strategy buys ETH (with BTC) when the ETH-BTC drops 5% below 50 days moving average (of a previous candle) + This example demonstrates: + - How to call Binance REST API for candle stick data + - How to incorporate external pricing source (Coingecko) into the strategy + - How to listen to order filled event + - How to structure order execution on a more complex strategy + Before running this example, make sure you run `config rate_oracle_source coingecko` + """ + connector_name: str = "binance_paper_trade" + trading_pair: str = "ETH-BTC" + base_asset, quote_asset = split_hb_trading_pair(trading_pair) + conversion_pair: str = f"{quote_asset}-USD" + buy_usd_amount: Decimal = Decimal("100") + moving_avg_period: int = 50 + dip_percentage: Decimal = Decimal("0.05") + #: A cool off period before the next buy (in seconds) + cool_off_interval: float = 10. + #: The last buy timestamp + last_ordered_ts: float = 0. + + markets = {connector_name: {trading_pair}} + + @property + def connector(self) -> ExchangeBase: + """ + The only connector in this strategy, define it here for easy access + """ + return self.connectors[self.connector_name] + + def on_tick(self): + """ + Runs every tick_size seconds, this is the main operation of the strategy. + - Create proposal (a list of order candidates) + - Check the account balance and adjust the proposal accordingly (lower order amount if needed) + - Lastly, execute the proposal on the exchange + """ + proposal: List[OrderCandidate] = self.create_proposal() + proposal = self.connector.budget_checker.adjust_candidates(proposal, all_or_none=False) + if proposal: + self.execute_proposal(proposal) + + def create_proposal(self) -> List[OrderCandidate]: + """ + Creates and returns a proposal (a list of order candidate), in this strategy the list has 1 element at most. + """ + daily_closes = self._get_daily_close_list(self.trading_pair) + start_index = (-1 * self.moving_avg_period) - 1 + # Calculate the average of the 50 element prior to the last element + avg_close = mean(daily_closes[start_index:-1]) + proposal = [] + # If the current price (the last close) is below the dip, add a new order candidate to the proposal + if daily_closes[-1] < avg_close * (Decimal("1") - self.dip_percentage): + order_price = self.connector.get_price(self.trading_pair, False) * Decimal("0.9") + usd_conversion_rate = RateOracle.get_instance().get_pair_rate(self.conversion_pair) + amount = (self.buy_usd_amount / usd_conversion_rate) / order_price + proposal.append(OrderCandidate(self.trading_pair, False, OrderType.LIMIT, TradeType.BUY, amount, + order_price)) + return proposal + + def execute_proposal(self, proposal: List[OrderCandidate]): + """ + Places the order candidates on the exchange, if it is not within cool off period and order candidate is valid. + """ + if self.last_ordered_ts > time.time() - self.cool_off_interval: + return + for order_candidate in proposal: + if order_candidate.amount > Decimal("0"): + self.buy(self.connector_name, self.trading_pair, order_candidate.amount, order_candidate.order_type, + order_candidate.price) + self.last_ordered_ts = time.time() + + def did_fill_order(self, event: OrderFilledEvent): + """ + Listens to fill order event to log it and notify the hummingbot application. + If you set up Telegram bot, you will get notification there as well. + """ + msg = (f"({event.trading_pair}) {event.trade_type.name} order (price: {event.price}) of {event.amount} " + f"{split_hb_trading_pair(event.trading_pair)[0]} is filled.") + self.log_with_clock(logging.INFO, msg) + self.notify_hb_app_with_timestamp(msg) + + def _get_daily_close_list(self, trading_pair: str) -> List[Decimal]: + """ + Fetches binance candle stick data and returns a list daily close + This is the API response data structure: + [ + [ + 1499040000000, // Open time + "0.01634790", // Open + "0.80000000", // High + "0.01575800", // Low + "0.01577100", // Close + "148976.11427815", // Volume + 1499644799999, // Close time + "2434.19055334", // Quote asset volume + 308, // Number of trades + "1756.87402397", // Taker buy base asset volume + "28.46694368", // Taker buy quote asset volume + "17928899.62484339" // Ignore. + ] + ] + + :param trading_pair: A market trading pair to + + :return: A list of daily close + """ + + url = "https://api.binance.com/api/v3/klines" + params = {"symbol": trading_pair.replace("-", ""), + "interval": "1d"} + records = requests.get(url=url, params=params).json() + return [Decimal(str(record[4])) for record in records] diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/buy_low_sell_high.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/buy_low_sell_high.py new file mode 100644 index 0000000..8f2c463 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/buy_low_sell_high.py @@ -0,0 +1,58 @@ +from collections import deque +from decimal import Decimal +from statistics import mean + +from hummingbot.core.data_type.common import OrderType +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class BuyLowSellHigh(ScriptStrategyBase): + """ + BotCamp Cohort: Sept 2022 + Design Template: https://hummingbot-foundation.notion.site/Buy-low-sell-high-35b89d84f0d94d379951a98f97179053 + Video: - + Description: + The script will be calculating the MA for a certain pair, and will execute a buy_order at the golden cross + and a sell_order at the death cross. + For the sake of simplicity in testing, we will define fast MA as the 5-secondly-MA, and slow MA as the + 20-secondly-MA. User can change this as desired + """ + markets = {"binance_paper_trade": {"BTC-USDT"}} + #: pingpong is a variable to allow alternating between buy & sell signals + pingpong = 0 + de_fast_ma = deque([], maxlen=5) + de_slow_ma = deque([], maxlen=20) + + def on_tick(self): + p = self.connectors["binance_paper_trade"].get_price("BTC-USDT", True) + + #: with every tick, the new price of the trading_pair will be appended to the deque and MA will be calculated + self.de_fast_ma.append(p) + self.de_slow_ma.append(p) + fast_ma = mean(self.de_fast_ma) + slow_ma = mean(self.de_slow_ma) + + #: logic for golden cross + if (fast_ma > slow_ma) & (self.pingpong == 0): + self.buy( + connector_name="binance_paper_trade", + trading_pair="BTC-USDT", + amount=Decimal(0.01), + order_type=OrderType.MARKET, + ) + self.logger().info(f'{"0.01 BTC bought"}') + self.pingpong = 1 + + #: logic for death cross + elif (slow_ma > fast_ma) & (self.pingpong == 1): + self.sell( + connector_name="binance_paper_trade", + trading_pair="BTC-USDT", + amount=Decimal(0.01), + order_type=OrderType.MARKET, + ) + self.logger().info(f'{"0.01 BTC sold"}') + self.pingpong = 0 + + else: + self.logger().info(f'{"wait for a signal to be generated"}') diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/buy_only_three_times_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/buy_only_three_times_example.py new file mode 100644 index 0000000..b0d31ee --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/buy_only_three_times_example.py @@ -0,0 +1,43 @@ +from decimal import Decimal + +from hummingbot.client.hummingbot_application import HummingbotApplication +from hummingbot.core.data_type.common import OrderType +from hummingbot.core.event.events import BuyOrderCreatedEvent +from hummingbot.core.rate_oracle.rate_oracle import RateOracle +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class BuyOnlyThreeTimesExample(ScriptStrategyBase): + """ + This example places shows how to add a logic to only place three buy orders in the market, + use an event to increase the counter and stop the strategy once the task is done. + """ + order_amount_usd = Decimal(100) + orders_created = 0 + orders_to_create = 3 + base = "ETH" + quote = "USDT" + markets = { + "kucoin_paper_trade": {f"{base}-{quote}"} + } + + def on_tick(self): + if self.orders_created < self.orders_to_create: + conversion_rate = RateOracle.get_instance().get_pair_rate(f"{self.base}-USD") + amount = self.order_amount_usd / conversion_rate + price = self.connectors["kucoin_paper_trade"].get_mid_price(f"{self.base}-{self.quote}") * Decimal(0.99) + self.buy( + connector_name="kucoin_paper_trade", + trading_pair="ETH-USDT", + amount=amount, + order_type=OrderType.LIMIT, + price=price + ) + + def did_create_buy_order(self, event: BuyOrderCreatedEvent): + trading_pair = f"{self.base}-{self.quote}" + if event.trading_pair == trading_pair: + self.orders_created += 1 + if self.orders_created == self.orders_to_create: + self.logger().info("All order created !") + HummingbotApplication.main_application().stop() diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/candles_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/candles_example.py new file mode 100644 index 0000000..7015980 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/candles_example.py @@ -0,0 +1,91 @@ +from typing import Dict + +import pandas as pd +import pandas_ta as ta # noqa: F401 + +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class CandlesExample(ScriptStrategyBase): + """ + This is a strategy that shows how to use the new Candlestick component. + It acquires data from both Binance spot and Binance perpetuals to initialize three different timeframes + of candlesticks. + The candlesticks are then displayed in the status, which is coded using a custom format status that + includes technical indicators. + This strategy serves as a clear example for other users on how to effectively utilize candlesticks in their own + trading strategies by utilizing the new Candlestick component. The integration of multiple timeframes and technical + indicators provides a comprehensive view of market trends and conditions, making this strategy a valuable tool for + informed trading decisions. + """ + # Available intervals: |1s|1m|3m|5m|15m|30m|1h|2h|4h|6h|8h|12h|1d|3d|1w|1M| + # Is possible to use the Candles Factory to create the candlestick that you want, and then you have to start it. + # Also, you can use the class directly like BinancePerpetualsCandles(trading_pair, interval, max_records), but + # this approach is better if you want to initialize multiple candles with a list or dict of configurations. + eth_1m_candles = CandlesFactory.get_candle(connector="binance", + trading_pair="ETH-USDT", + interval="1m", max_records=500) + eth_1h_candles = CandlesFactory.get_candle(connector="binance_perpetual", + trading_pair="ETH-USDT", + interval="1h", max_records=500) + eth_1w_candles = CandlesFactory.get_candle(connector="binance_perpetual", + trading_pair="ETH-USDT", + interval="1w", max_records=50) + + # The markets are the connectors that you can use to execute all the methods of the scripts strategy base + # The candlesticks are just a component that provides the information of the candlesticks + markets = {"binance_paper_trade": {"SOL-USDT"}} + + def __init__(self, connectors: Dict[str, ConnectorBase]): + # Is necessary to start the Candles Feed. + super().__init__(connectors) + self.eth_1m_candles.start() + self.eth_1h_candles.start() + self.eth_1w_candles.start() + + @property + def all_candles_ready(self): + """ + Checks if the candlesticks are full. + :return: + """ + return all([self.eth_1h_candles.is_ready, self.eth_1m_candles.is_ready, self.eth_1w_candles.is_ready]) + + def on_tick(self): + pass + + def on_stop(self): + """ + Without this functionality, the network iterator will continue running forever after stopping the strategy + That's why is necessary to introduce this new feature to make a custom stop with the strategy. + :return: + """ + self.eth_1m_candles.stop() + self.eth_1h_candles.stop() + self.eth_1w_candles.stop() + + def format_status(self) -> str: + """ + Displays the three candlesticks involved in the script with RSI, BBANDS and EMA. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + if self.all_candles_ready: + lines.extend(["\n############################################ Market Data ############################################\n"]) + for candles in [self.eth_1w_candles, self.eth_1m_candles, self.eth_1h_candles]: + candles_df = candles.candles_df + # Let's add some technical indicators + candles_df.ta.rsi(length=14, append=True) + candles_df.ta.bbands(length=20, std=2, append=True) + candles_df.ta.ema(length=14, offset=None, append=True) + candles_df["timestamp"] = pd.to_datetime(candles_df["timestamp"], unit="ms") + lines.extend([f"Candles: {candles.name} | Interval: {candles.interval}"]) + lines.extend([" " + line for line in candles_df.tail().to_string(index=False).split("\n")]) + lines.extend(["\n-----------------------------------------------------------------------------------------------------------\n"]) + else: + lines.extend(["", " No data collected."]) + + return "\n".join(lines) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/clob_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/clob_example.py new file mode 100644 index 0000000..076ebc0 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/clob_example.py @@ -0,0 +1,5 @@ +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class CLOBSerumExample(ScriptStrategyBase): + pass diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/dca_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/dca_example.py new file mode 100644 index 0000000..36bcbcc --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/dca_example.py @@ -0,0 +1,77 @@ +import logging + +from hummingbot.core.event.events import ( + BuyOrderCompletedEvent, + BuyOrderCreatedEvent, + MarketOrderFailureEvent, + OrderCancelledEvent, + OrderFilledEvent, + SellOrderCompletedEvent, + SellOrderCreatedEvent, +) +from hummingbot.strategy.script_strategy_base import Decimal, OrderType, ScriptStrategyBase + + +class DCAExample(ScriptStrategyBase): + """ + This example shows how to set up a simple strategy to buy a token on fixed (dollar) amount on a regular basis + """ + #: Define markets to instruct Hummingbot to create connectors on the exchanges and markets you need + markets = {"binance_paper_trade": {"BTC-USDT"}} + #: The last time the strategy places a buy order + last_ordered_ts = 0. + #: Buying interval (in seconds) + buy_interval = 10. + #: Buying amount (in dollars - USDT) + buy_quote_amount = Decimal("100") + + def on_tick(self): + # Check if it is time to buy + if self.last_ordered_ts < (self.current_timestamp - self.buy_interval): + # Lets set the order price to the best bid + price = self.connectors["binance_paper_trade"].get_price("BTC-USDT", False) + amount = self.buy_quote_amount / price + self.buy("binance_paper_trade", "BTC-USDT", amount, OrderType.LIMIT, price) + self.last_ordered_ts = self.current_timestamp + + def did_create_buy_order(self, event: BuyOrderCreatedEvent): + """ + Method called when the connector notifies a buy order has been created + """ + self.logger().info(logging.INFO, f"The buy order {event.order_id} has been created") + + def did_create_sell_order(self, event: SellOrderCreatedEvent): + """ + Method called when the connector notifies a sell order has been created + """ + self.logger().info(logging.INFO, f"The sell order {event.order_id} has been created") + + def did_fill_order(self, event: OrderFilledEvent): + """ + Method called when the connector notifies that an order has been partially or totally filled (a trade happened) + """ + self.logger().info(logging.INFO, f"The order {event.order_id} has been filled") + + def did_fail_order(self, event: MarketOrderFailureEvent): + """ + Method called when the connector notifies an order has failed + """ + self.logger().info(logging.INFO, f"The order {event.order_id} failed") + + def did_cancel_order(self, event: OrderCancelledEvent): + """ + Method called when the connector notifies an order has been cancelled + """ + self.logger().info(f"The order {event.order_id} has been cancelled") + + def did_complete_buy_order(self, event: BuyOrderCompletedEvent): + """ + Method called when the connector notifies a buy order has been completed (fully filled) + """ + self.logger().info(f"The buy order {event.order_id} has been completed") + + def did_complete_sell_order(self, event: SellOrderCompletedEvent): + """ + Method called when the connector notifies a sell order has been completed (fully filled) + """ + self.logger().info(f"The sell order {event.order_id} has been completed") diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_bb_rsi_multi_timeframe.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_bb_rsi_multi_timeframe.py new file mode 100644 index 0000000..7b39cc5 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_bb_rsi_multi_timeframe.py @@ -0,0 +1,120 @@ +from decimal import Decimal + +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase + + +class MultiTimeframeBBRSI(DirectionalStrategyBase): + """ + MultiTimeframeBBRSI strategy implementation based on the DirectionalStrategyBase. + + This strategy combines multiple timeframes of Bollinger Bands (BB) and Relative Strength Index (RSI) indicators to + generate trading signals and execute trades based on the composed signal value. It defines the specific parameters + and configurations for the MultiTimeframeBBRSI strategy. + + Parameters: + directional_strategy_name (str): The name of the strategy. + trading_pair (str): The trading pair to be traded. + exchange (str): The exchange to be used for trading. + order_amount_usd (Decimal): The amount of the order in USD. + leverage (int): The leverage to be used for trading. + + Position Parameters: + stop_loss (float): The stop-loss percentage for the position. + take_profit (float): The take-profit percentage for the position. + time_limit (int or None): The time limit for the position in seconds. Set to `None` for no time limit. + trailing_stop_activation_delta (float): The activation delta for the trailing stop. + trailing_stop_trailing_delta (float): The trailing delta for the trailing stop. + + Candlestick Configuration: + candles (List[CandlesBase]): The list of candlesticks used for generating signals. + + Markets: + A dictionary specifying the markets and trading pairs for the strategy. + + Inherits from: + DirectionalStrategyBase: Base class for creating directional strategies using the PositionExecutor. + """ + directional_strategy_name: str = "bb_rsi_multi_timeframe" + # Define the trading pair and exchange that we want to use and the csv where we are going to store the entries + trading_pair: str = "ETH-USDT" + exchange: str = "binance_perpetual" + order_amount_usd = Decimal("20") + leverage = 10 + + # Configure the parameters for the position + stop_loss: float = 0.0075 + take_profit: float = 0.015 + time_limit: int = None + trailing_stop_activation_delta = 0.004 + trailing_stop_trailing_delta = 0.001 + + candles = [ + CandlesFactory.get_candle(connector=exchange, + trading_pair=trading_pair, + interval="1m", max_records=150), + CandlesFactory.get_candle(connector=exchange, + trading_pair=trading_pair, + interval="3m", max_records=150), + ] + markets = {exchange: {trading_pair}} + + def get_signal(self): + """ + Generates the trading signal based on the composed signal value from multiple timeframes. + Returns: + int: The trading signal (-1 for sell, 0 for hold, 1 for buy). + """ + signals = [] + for candle in self.candles: + candles_df = self.get_processed_df(candle.candles_df) + last_row = candles_df.iloc[-1] + # We are going to normalize the values of the signals between -1 and 1. + # -1 --> short | 1 --> long, so in the normalization we also need to switch side by changing the sign + sma_rsi_normalized = -1 * (last_row["RSI_21_SMA_10"].item() - 50) / 50 + bb_percentage_normalized = -1 * (last_row["BBP_21_2.0"].item() - 0.5) / 0.5 + # we assume that the weigths of sma of rsi and bb are equal + signal_value = (sma_rsi_normalized + bb_percentage_normalized) / 2 + signals.append(signal_value) + # Here we have a list with the values of the signals for each candle + # The idea is that you can define rules between the signal values of multiple trading pairs or timeframes + # In this example, we are going to prioritize the short term signal, so the weight of the 1m candle + # is going to be 0.7 and the weight of the 3m candle 0.3 + composed_signal_value = 0.7 * signals[0] + 0.3 * signals[1] + # Here we are applying thresholds to the composed signal value + if composed_signal_value > 0.5: + return 1 + elif composed_signal_value < -0.5: + return -1 + else: + return 0 + + @staticmethod + def get_processed_df(candles): + """ + Retrieves the processed dataframe with Bollinger Bands and RSI values for a specific candlestick. + Args: + candles (pd.DataFrame): The raw candlestick dataframe. + Returns: + pd.DataFrame: The processed dataframe with Bollinger Bands and RSI values. + """ + candles_df = candles.copy() + # Let's add some technical indicators + candles_df.ta.bbands(length=21, append=True) + candles_df.ta.rsi(length=21, append=True) + candles_df.ta.sma(length=10, close="RSI_21", prefix="RSI_21", append=True) + return candles_df + + def market_data_extra_info(self): + """ + Provides additional information about the market data for each candlestick. + Returns: + List[str]: A list of formatted strings containing market data information. + """ + lines = [] + columns_to_show = ["timestamp", "open", "low", "high", "close", "volume", "RSI_21_SMA_10", "BBP_21_2.0"] + for candle in self.candles: + candles_df = self.get_processed_df(candle.candles_df) + lines.extend([f"Candles: {candle.name} | Interval: {candle.interval}\n"]) + lines.extend(self.candles_formatted_list(candles_df, columns_to_show)) + return lines diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_macd_bb.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_macd_bb.py new file mode 100644 index 0000000..e0ad856 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_macd_bb.py @@ -0,0 +1,98 @@ +from decimal import Decimal + +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase + + +class MacdBB(DirectionalStrategyBase): + """ + MacdBB strategy implementation based on the DirectionalStrategyBase. + + This strategy combines the MACD (Moving Average Convergence Divergence) and Bollinger Bands indicators to generate + trading signals and execute trades based on the indicator values. It defines the specific parameters and + configurations for the MacdBB strategy. + + Parameters: + directional_strategy_name (str): The name of the strategy. + trading_pair (str): The trading pair to be traded. + exchange (str): The exchange to be used for trading. + order_amount_usd (Decimal): The amount of the order in USD. + leverage (int): The leverage to be used for trading. + + Position Parameters: + stop_loss (float): The stop-loss percentage for the position. + take_profit (float): The take-profit percentage for the position. + time_limit (int): The time limit for the position in seconds. + trailing_stop_activation_delta (float): The activation delta for the trailing stop. + trailing_stop_trailing_delta (float): The trailing delta for the trailing stop. + + Candlestick Configuration: + candles (List[CandlesBase]): The list of candlesticks used for generating signals. + + Markets: + A dictionary specifying the markets and trading pairs for the strategy. + + Inherits from: + DirectionalStrategyBase: Base class for creating directional strategies using the PositionExecutor. + """ + directional_strategy_name: str = "MACD_BB" + # Define the trading pair and exchange that we want to use and the csv where we are going to store the entries + trading_pair: str = "BTC-USDT" + exchange: str = "binance_perpetual" + order_amount_usd = Decimal("20") + leverage = 10 + + # Configure the parameters for the position + stop_loss: float = 0.0075 + take_profit: float = 0.015 + time_limit: int = 60 * 55 + trailing_stop_activation_delta = 0.003 + trailing_stop_trailing_delta = 0.0007 + + candles = [CandlesFactory.get_candle(connector=exchange, + trading_pair=trading_pair, + interval="3m", max_records=150)] + markets = {exchange: {trading_pair}} + + def get_signal(self): + """ + Generates the trading signal based on the MACD and Bollinger Bands indicators. + Returns: + int: The trading signal (-1 for sell, 0 for hold, 1 for buy). + """ + candles_df = self.get_processed_df() + last_candle = candles_df.iloc[-1] + bbp = last_candle["BBP_100_2.0"] + macdh = last_candle["MACDh_21_42_9"] + macd = last_candle["MACD_21_42_9"] + if bbp < 0.4 and macdh > 0 and macd < 0: + signal_value = 1 + elif bbp > 0.6 and macdh < 0 and macd > 0: + signal_value = -1 + else: + signal_value = 0 + return signal_value + + def get_processed_df(self): + """ + Retrieves the processed dataframe with MACD and Bollinger Bands values. + Returns: + pd.DataFrame: The processed dataframe with MACD and Bollinger Bands values. + """ + candles_df = self.candles[0].candles_df + candles_df.ta.bbands(length=100, append=True) + candles_df.ta.macd(fast=21, slow=42, signal=9, append=True) + return candles_df + + def market_data_extra_info(self): + """ + Provides additional information about the market data. + Returns: + List[str]: A list of formatted strings containing market data information. + """ + lines = [] + columns_to_show = ["timestamp", "open", "low", "high", "close", "volume", "BBP_100_2.0", "MACDh_21_42_9", "MACD_21_42_9"] + candles_df = self.get_processed_df() + lines.extend([f"Candles: {self.candles[0].name} | Interval: {self.candles[0].interval}\n"]) + lines.extend(self.candles_formatted_list(candles_df, columns_to_show)) + return lines diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_rsi.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_rsi.py new file mode 100644 index 0000000..c9c9d82 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_rsi.py @@ -0,0 +1,98 @@ +from decimal import Decimal + +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase + + +class RSI(DirectionalStrategyBase): + """ + RSI (Relative Strength Index) strategy implementation based on the DirectionalStrategyBase. + + This strategy uses the RSI indicator to generate trading signals and execute trades based on the RSI values. + It defines the specific parameters and configurations for the RSI strategy. + + Parameters: + directional_strategy_name (str): The name of the strategy. + trading_pair (str): The trading pair to be traded. + exchange (str): The exchange to be used for trading. + order_amount_usd (Decimal): The amount of the order in USD. + leverage (int): The leverage to be used for trading. + + Position Parameters: + stop_loss (float): The stop-loss percentage for the position. + take_profit (float): The take-profit percentage for the position. + time_limit (int): The time limit for the position in seconds. + trailing_stop_activation_delta (float): The activation delta for the trailing stop. + trailing_stop_trailing_delta (float): The trailing delta for the trailing stop. + + Candlestick Configuration: + candles (List[CandlesBase]): The list of candlesticks used for generating signals. + + Markets: + A dictionary specifying the markets and trading pairs for the strategy. + + Methods: + get_signal(): Generates the trading signal based on the RSI indicator. + get_processed_df(): Retrieves the processed dataframe with RSI values. + market_data_extra_info(): Provides additional information about the market data. + + Inherits from: + DirectionalStrategyBase: Base class for creating directional strategies using the PositionExecutor. + """ + directional_strategy_name: str = "RSI" + # Define the trading pair and exchange that we want to use and the csv where we are going to store the entries + trading_pair: str = "ETH-USDT" + exchange: str = "binance_perpetual" + order_amount_usd = Decimal("20") + leverage = 10 + + # Configure the parameters for the position + stop_loss: float = 0.0075 + take_profit: float = 0.015 + time_limit: int = 60 * 1 + trailing_stop_activation_delta = 0.004 + trailing_stop_trailing_delta = 0.001 + cooldown_after_execution = 10 + + candles = [CandlesFactory.get_candle(connector=exchange, + trading_pair=trading_pair, + interval="1m", max_records=150)] + markets = {exchange: {trading_pair}} + + def get_signal(self): + """ + Generates the trading signal based on the RSI indicator. + Returns: + int: The trading signal (-1 for sell, 0 for hold, 1 for buy). + """ + candles_df = self.get_processed_df() + rsi_value = candles_df.iat[-1, -1] + if rsi_value > 70: + return -1 + elif rsi_value < 30: + return 1 + else: + return 0 + + def get_processed_df(self): + """ + Retrieves the processed dataframe with RSI values. + Returns: + pd.DataFrame: The processed dataframe with RSI values. + """ + candles_df = self.candles[0].candles_df + candles_df.ta.rsi(length=7, append=True) + return candles_df + + def market_data_extra_info(self): + """ + Provides additional information about the market data to the format status. + Returns: + List[str]: A list of formatted strings containing market data information. + """ + lines = [] + columns_to_show = ["timestamp", "open", "low", "high", "close", "volume", "RSI_7"] + candles_df = self.get_processed_df() + lines.extend([f"Candles: {self.candles[0].name} | Interval: {self.candles[0].interval}\n"]) + lines.extend(self.candles_formatted_list(candles_df, columns_to_show)) + return lines diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_rsi_spot.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_rsi_spot.py new file mode 100644 index 0000000..22c54d2 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_rsi_spot.py @@ -0,0 +1,97 @@ +from decimal import Decimal + +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase + + +class RSISpot(DirectionalStrategyBase): + """ + RSI (Relative Strength Index) strategy implementation based on the DirectionalStrategyBase. + + This strategy uses the RSI indicator to generate trading signals and execute trades based on the RSI values. + It defines the specific parameters and configurations for the RSI strategy. + + Parameters: + directional_strategy_name (str): The name of the strategy. + trading_pair (str): The trading pair to be traded. + exchange (str): The exchange to be used for trading. + order_amount_usd (Decimal): The amount of the order in USD. + leverage (int): The leverage to be used for trading. + + Position Parameters: + stop_loss (float): The stop-loss percentage for the position. + take_profit (float): The take-profit percentage for the position. + time_limit (int): The time limit for the position in seconds. + trailing_stop_activation_delta (float): The activation delta for the trailing stop. + trailing_stop_trailing_delta (float): The trailing delta for the trailing stop. + + Candlestick Configuration: + candles (List[CandlesBase]): The list of candlesticks used for generating signals. + + Markets: + A dictionary specifying the markets and trading pairs for the strategy. + + Methods: + get_signal(): Generates the trading signal based on the RSI indicator. + get_processed_df(): Retrieves the processed dataframe with RSI values. + market_data_extra_info(): Provides additional information about the market data. + + Inherits from: + DirectionalStrategyBase: Base class for creating directional strategies using the PositionExecutor. + """ + directional_strategy_name: str = "RSI_spot" + # Define the trading pair and exchange that we want to use and the csv where we are going to store the entries + trading_pair: str = "ETH-USDT" + exchange: str = "binance" + order_amount_usd = Decimal("20") + leverage = 10 + + # Configure the parameters for the position + stop_loss: float = 0.0075 + take_profit: float = 0.015 + time_limit: int = 60 * 55 + trailing_stop_activation_delta = 0.004 + trailing_stop_trailing_delta = 0.001 + + candles = [CandlesFactory.get_candle(connector=exchange, + trading_pair=trading_pair, + interval="1m", max_records=150)] + markets = {exchange: {trading_pair}} + + def get_signal(self): + """ + Generates the trading signal based on the RSI indicator. + Returns: + int: The trading signal (-1 for sell, 0 for hold, 1 for buy). + """ + candles_df = self.get_processed_df() + rsi_value = candles_df.iat[-1, -1] + if rsi_value > 70: + return -1 + elif rsi_value < 30: + return 1 + else: + return 0 + + def get_processed_df(self): + """ + Retrieves the processed dataframe with RSI values. + Returns: + pd.DataFrame: The processed dataframe with RSI values. + """ + candles_df = self.candles[0].candles_df + candles_df.ta.rsi(length=7, append=True) + return candles_df + + def market_data_extra_info(self): + """ + Provides additional information about the market data to the format status. + Returns: + List[str]: A list of formatted strings containing market data information. + """ + lines = [] + columns_to_show = ["timestamp", "open", "low", "high", "close", "volume", "RSI_7"] + candles_df = self.get_processed_df() + lines.extend([f"Candles: {self.candles[0].name} | Interval: {self.candles[0].interval}\n"]) + lines.extend(self.candles_formatted_list(candles_df, columns_to_show)) + return lines diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_stat_arb.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_stat_arb.py new file mode 100644 index 0000000..e4c703e --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_stat_arb.py @@ -0,0 +1,78 @@ +from decimal import Decimal +import pandas as pd +import pandas_ta as ta + +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase + + +class StatArb(DirectionalStrategyBase): + directional_strategy_name: str = "STAT_ARB" + # Define the trading pair and exchange that we want to use and the csv where we are going to store the entries + trading_pair: str = "ETH-USDT" + exchange: str = "binance_perpetual" + order_amount_usd = Decimal("15") + leverage = 10 + + # Configure the parameters for the position + stop_loss: float = 0.025 + take_profit: float = 0.02 + time_limit: int = 60 * 60 * 12 + trailing_stop_activation_delta = 0.01 + trailing_stop_trailing_delta = 0.005 + cooldown_after_execution = 10 + max_executors = 1 + + candles = [CandlesFactory.get_candle(connector="binance_perpetual", + trading_pair=trading_pair, + interval="1h", max_records=150), + CandlesFactory.get_candle(connector="binance_perpetual", + trading_pair="BTC-USDT", + interval="1h", max_records=150) + ] + periods = 24 + markets = {exchange: {trading_pair}} + + def get_signal(self): + """ + Generates the trading signal based on the RSI indicator. + Returns: + int: The trading signal (-1 for sell, 0 for hold, 1 for buy). + """ + candles_df = self.get_processed_df() + z_score = candles_df["z_score"].iloc[-1] + if z_score > 1.1: + return 1 + elif z_score < -1.1: + return -1 + else: + return 0 + + def get_processed_df(self): + """ + Retrieves the processed dataframe with RSI values. + Returns: + pd.DataFrame: The processed dataframe with RSI values. + """ + candles_df_eth = self.candles[0].candles_df + candles_df_btc = self.candles[1].candles_df + df = pd.merge(candles_df_eth, candles_df_btc, on="timestamp", how='inner', suffixes=('', '_target')) + df["pct_change_original"] = df["close"].pct_change() + df["pct_change_target"] = df["close_target"].pct_change() + df["spread"] = df["pct_change_target"] - df["pct_change_original"] + df["cum_spread"] = df["spread"].rolling(self.periods).sum() + df["z_score"] = ta.zscore(df["cum_spread"], length=self.periods) + return df + + def market_data_extra_info(self): + """ + Provides additional information about the market data to the format status. + Returns: + List[str]: A list of formatted strings containing market data information. + """ + lines = [] + columns_to_show = ["timestamp", "open", "low", "high", "close", "volume", "z_score", "cum_spread", "close_target"] + candles_df = self.get_processed_df() + lines.extend([f"Candles: {self.candles[0].name} | Interval: {self.candles[0].interval}\n"]) + lines.extend(self.candles_formatted_list(candles_df, columns_to_show)) + return lines diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_trend_follower.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_trend_follower.py new file mode 100644 index 0000000..339f6fc --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_trend_follower.py @@ -0,0 +1,73 @@ +from decimal import Decimal + +from hummingbot.core.data_type.common import OrderType +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase + + +class TrendFollowingStrategy(DirectionalStrategyBase): + directional_strategy_name = "trend_following" + trading_pair = "DOGE-USDT" + exchange = "binance_perpetual" + order_amount_usd = Decimal("20") + leverage = 10 + + # Configure the parameters for the position + stop_loss: float = 0.01 + take_profit: float = 0.05 + time_limit: int = 60 * 60 * 3 + open_order_type = OrderType.MARKET + take_profit_order_type: OrderType = OrderType.MARKET + trailing_stop_activation_delta = 0.01 + trailing_stop_trailing_delta = 0.003 + candles = [CandlesFactory.get_candle(connector=exchange, + trading_pair=trading_pair, + interval="3m", max_records=250)] + markets = {exchange: {trading_pair}} + + def get_signal(self): + """ + Generates the trading signal based on the MACD and Bollinger Bands indicators. + Returns: + int: The trading signal (-1 for sell, 0 for hold, 1 for buy). + """ + candles_df = self.get_processed_df() + last_candle = candles_df.iloc[-1] + bbp = last_candle["BBP_100_2.0"] + sma_21 = last_candle["SMA_21"] + sma_200 = last_candle["SMA_200"] + trend = sma_21 > sma_200 + filter = (bbp > 0.35) and (bbp < 0.65) + + if trend and filter: + signal_value = 1 + elif not trend and filter: + signal_value = -1 + else: + signal_value = 0 + return signal_value + + def get_processed_df(self): + """ + Retrieves the processed dataframe with MACD and Bollinger Bands values. + Returns: + pd.DataFrame: The processed dataframe with MACD and Bollinger Bands values. + """ + candles_df = self.candles[0].candles_df + candles_df.ta.sma(length=21, append=True) + candles_df.ta.sma(length=200, append=True) + candles_df.ta.bbands(length=100, append=True) + return candles_df + + def market_data_extra_info(self): + """ + Provides additional information about the market data. + Returns: + List[str]: A list of formatted strings containing market data information. + """ + lines = [] + columns_to_show = ["timestamp", "open", "low", "high", "close", "volume", "BBP_100_2.0", "SMA_21", "SMA_200"] + candles_df = self.get_processed_df() + lines.extend([f"Candles: {self.candles[0].name} | Interval: {self.candles[0].interval}\n"]) + lines.extend(self.candles_formatted_list(candles_df, columns_to_show)) + return lines diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_widening_ema_bands.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_widening_ema_bands.py new file mode 100644 index 0000000..9e6c2ba --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/directional_strategy_widening_ema_bands.py @@ -0,0 +1,101 @@ +from decimal import Decimal + +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase + + +class WideningEMABands(DirectionalStrategyBase): + """ + WideningEMABands strategy implementation based on the DirectionalStrategyBase. + + This strategy uses two EMAs one short and one long to generate trading signals and execute trades based on the + percentage of distance between them. + + Parameters: + directional_strategy_name (str): The name of the strategy. + trading_pair (str): The trading pair to be traded. + exchange (str): The exchange to be used for trading. + order_amount_usd (Decimal): The amount of the order in USD. + leverage (int): The leverage to be used for trading. + distance_pct_threshold (float): The percentage of distance between the EMAs to generate a signal. + + Position Parameters: + stop_loss (float): The stop-loss percentage for the position. + take_profit (float): The take-profit percentage for the position. + time_limit (int): The time limit for the position in seconds. + trailing_stop_activation_delta (float): The activation delta for the trailing stop. + trailing_stop_trailing_delta (float): The trailing delta for the trailing stop. + + Candlestick Configuration: + candles (List[CandlesBase]): The list of candlesticks used for generating signals. + + Markets: + A dictionary specifying the markets and trading pairs for the strategy. + + Inherits from: + DirectionalStrategyBase: Base class for creating directional strategies using the PositionExecutor. + """ + directional_strategy_name: str = "Widening_EMA_Bands" + # Define the trading pair and exchange that we want to use and the csv where we are going to store the entries + trading_pair: str = "LINA-USDT" + exchange: str = "binance_perpetual" + order_amount_usd = Decimal("20") + leverage = 10 + distance_pct_threshold = 0.02 + + # Configure the parameters for the position + stop_loss: float = 0.015 + take_profit: float = 0.03 + time_limit: int = 60 * 60 * 5 + trailing_stop_activation_delta = 0.008 + trailing_stop_trailing_delta = 0.003 + + candles = [CandlesFactory.get_candle(connector=exchange, + trading_pair=trading_pair, + interval="3m", max_records=150)] + markets = {exchange: {trading_pair}} + + def get_signal(self): + """ + Generates the trading signal based on the MACD and Bollinger Bands indicators. + Returns: + int: The trading signal (-1 for sell, 0 for hold, 1 for buy). + """ + candles_df = self.get_processed_df() + last_candle = candles_df.iloc[-1] + ema_8 = last_candle["EMA_8"] + ema_54 = last_candle["EMA_54"] + distance = ema_8 - ema_54 + average = (ema_8 + ema_54) / 2 + distance_pct = distance / average + if distance_pct > self.distance_pct_threshold: + signal_value = -1 + elif distance_pct < -self.distance_pct_threshold: + signal_value = 1 + else: + signal_value = 0 + return signal_value + + def get_processed_df(self): + """ + Retrieves the processed dataframe with MACD and Bollinger Bands values. + Returns: + pd.DataFrame: The processed dataframe with MACD and Bollinger Bands values. + """ + candles_df = self.candles[0].candles_df + candles_df.ta.ema(length=8, append=True) + candles_df.ta.ema(length=54, append=True) + return candles_df + + def market_data_extra_info(self): + """ + Provides additional information about the market data. + Returns: + List[str]: A list of formatted strings containing market data information. + """ + lines = [] + columns_to_show = ["timestamp", "open", "low", "high", "close", "volume", "EMA_8", "EMA_54"] + candles_df = self.get_processed_df() + lines.extend([f"Candles: {self.candles[0].name} | Interval: {self.candles[0].interval}\n"]) + lines.extend(self.candles_formatted_list(candles_df, columns_to_show)) + return lines diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/download_candles.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/download_candles.py new file mode 100644 index 0000000..e9501fd --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/download_candles.py @@ -0,0 +1,62 @@ +import os +from typing import Dict + +from hummingbot import data_path +from hummingbot.client.hummingbot_application import HummingbotApplication +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class DownloadCandles(ScriptStrategyBase): + """ + This script provides an example of how to use the Candles Feed to download and store historical data. + It downloads 3-minute candles for 3 Binance trading pairs ["APE-USDT", "BTC-USDT", "BNB-USDT"] and stores them in + CSV files in the /data directory. The script stops after it has downloaded 50,000 max_records records for each pair. + Is important to notice that the component will fail if all the candles are not available since the idea of it is to + use it in production based on candles needed to compute technical indicators. + """ + exchange = os.getenv("EXCHANGE", "binance_perpetual") + trading_pairs = os.getenv("TRADING_PAIRS", "DODO-BUSD,LTC-USDT").split(",") + intervals = os.getenv("INTERVALS", "1m,3m,5m,1h").split(",") + days_to_download = int(os.getenv("DAYS_TO_DOWNLOAD", "3")) + # we can initialize any trading pair since we only need the candles + markets = {"binance_paper_trade": {"BTC-USDT"}} + + @staticmethod + def get_max_records(days_to_download: int, interval: str) -> int: + conversion = {"m": 1, "h": 60, "d": 1440} + unit = interval[-1] + quantity = int(interval[:-1]) + return int(days_to_download * 24 * 60 / (quantity * conversion[unit])) + + def __init__(self, connectors: Dict[str, ConnectorBase]): + super().__init__(connectors) + combinations = [(trading_pair, interval) for trading_pair in self.trading_pairs for interval in self.intervals] + + self.candles = {f"{combinations[0]}_{combinations[1]}": {} for combinations in combinations} + # we need to initialize the candles for each trading pair + for combination in combinations: + candle = CandlesFactory.get_candle(connector=self.exchange, trading_pair=combination[0], + interval=combination[1], + max_records=self.get_max_records(self.days_to_download, combination[1])) + candle.start() + # we are storing the candles object and the csv path to save the candles + self.candles[f"{combination[0]}_{combination[1]}"]["candles"] = candle + self.candles[f"{combination[0]}_{combination[1]}"][ + "csv_path"] = data_path() + f"/candles_{self.exchange}_{combination[0]}_{combination[1]}.csv" + + def on_tick(self): + for trading_pair, candles_info in self.candles.items(): + if not candles_info["candles"].is_ready: + self.logger().info(f"Candles not ready yet for {trading_pair}! Missing {candles_info['candles']._candles.maxlen - len(candles_info['candles']._candles)}") + pass + else: + df = candles_info["candles"].candles_df + df.to_csv(candles_info["csv_path"], index=False) + if all(candles_info["candles"].is_ready for candles_info in self.candles.values()): + HummingbotApplication.main_application().stop() + + def on_stop(self): + for candles_info in self.candles.values(): + candles_info["candles"].stop() diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/external_events_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/external_events_example.py new file mode 100644 index 0000000..7695497 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/external_events_example.py @@ -0,0 +1,74 @@ +from hummingbot.core.event.events import BuyOrderCreatedEvent, MarketOrderFailureEvent, SellOrderCreatedEvent +from hummingbot.remote_iface.mqtt import ExternalEventFactory, ExternalTopicFactory +from hummingbot.strategy.script_strategy_base import Decimal, OrderType, ScriptStrategyBase + + +class ExternalEventsExample(ScriptStrategyBase): + """ + Simple script that uses the external events plugin to create buy and sell + market orders. + """ + #: Define markets + markets = {"kucoin_paper_trade": {"BTC-USDT"}} + + # ------ Using Factory Classes ------ + # hbot/{id}/external/events/* + eevents = ExternalEventFactory.create_queue('*') + # hbot/{id}/test/a + etopic_queue = ExternalTopicFactory.create_queue('test/a') + + # ---- Using callback functions ---- + # ---------------------------------- + def __init__(self, *args, **kwargs): + ExternalEventFactory.create_async('*', self.on_event) + self.listener = ExternalTopicFactory.create_async('test/a', self.on_message) + super().__init__(*args, **kwargs) + + def on_event(self, msg, name): + self.logger().info(f'OnEvent Callback fired: {name} -> {msg}') + + def on_message(self, msg, topic): + self.logger().info(f'Topic Message Callback fired: {topic} -> {msg}') + + def on_stop(self): + ExternalEventFactory.remove_listener('*', self.on_event) + ExternalTopicFactory.remove_listener(self.listener) + # ---------------------------------- + + def on_tick(self): + while len(self.eevents) > 0: + event = self.eevents.popleft() + self.logger().info(f'External Event in Queue: {event}') + # event = (name, msg) + if event[0] == 'order.market': + if event[1].data['type'] in ('buy', 'Buy', 'BUY'): + self.execute_order(Decimal(event[1].data['amount']), True) + elif event[1].data['type'] in ('sell', 'Sell', 'SELL'): + self.execute_order(Decimal(event[1].data['amount']), False) + while len(self.etopic_queue) > 0: + entry = self.etopic_queue.popleft() + self.logger().info(f'Topic Message in Queue: {entry[0]} -> {entry[1]}') + + def execute_order(self, amount: Decimal, is_buy: bool): + if is_buy: + self.buy("kucoin_paper_trade", "BTC-USDT", amount, OrderType.MARKET) + else: + self.sell("kucoin_paper_trade", "BTC-USDT", amount, OrderType.MARKET) + + def did_create_buy_order(self, event: BuyOrderCreatedEvent): + """ + Method called when the connector notifies a buy order has been created + """ + self.logger().info(f"The buy order {event.order_id} has been created") + + def did_create_sell_order(self, event: SellOrderCreatedEvent): + """ + Method called when the connector notifies a sell order has been created + """ + self.logger().info(f"The sell order {event.order_id} has been created") + + def did_fail_order(self, event: MarketOrderFailureEvent): + """ + Method called when the connector notifies an order has failed + """ + self.logger().info(f"The order {event.order_id} failed") diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/format_status_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/format_status_example.py new file mode 100644 index 0000000..7ff9b1f --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/format_status_example.py @@ -0,0 +1,47 @@ +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class FormatStatusExample(ScriptStrategyBase): + """ + This example shows how to add a custom format_status to a strategy and query the order book. + Run the command status --live, once the strategy starts. + """ + markets = { + "binance_paper_trade": {"ETH-USDT"}, + "kucoin_paper_trade": {"ETH-USDT"}, + "gate_io_paper_trade": {"ETH-USDT"}, + } + + def format_status(self) -> str: + """ + Returns status of the current strategy on user balances and current active orders. This function is called + when status command is issued. Override this function to create custom status display output. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + warning_lines = [] + warning_lines.extend(self.network_warning(self.get_market_trading_pair_tuples())) + + balance_df = self.get_balance_df() + lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")]) + market_status_df = self.get_market_status_df_with_depth() + lines.extend(["", " Market Status Data Frame:"] + [" " + line for line in market_status_df.to_string(index=False).split("\n")]) + + warning_lines.extend(self.balance_warning(self.get_market_trading_pair_tuples())) + if len(warning_lines) > 0: + lines.extend(["", "*** WARNINGS ***"] + warning_lines) + return "\n".join(lines) + + def get_market_status_df_with_depth(self): + market_status_df = self.market_status_data_frame(self.get_market_trading_pair_tuples()) + market_status_df["Exchange"] = market_status_df.apply(lambda x: x["Exchange"].strip("PaperTrade") + "paper_trade", axis=1) + market_status_df["Volume (+1%)"] = market_status_df.apply(lambda x: self.get_volume_for_percentage_from_mid_price(x, 0.01), axis=1) + market_status_df["Volume (-1%)"] = market_status_df.apply(lambda x: self.get_volume_for_percentage_from_mid_price(x, -0.01), axis=1) + return market_status_df + + def get_volume_for_percentage_from_mid_price(self, row, percentage): + price = row["Mid Price"] * (1 + percentage) + is_buy = percentage > 0 + result = self.connectors[row["Exchange"]].get_volume_for_price(row["Market"], is_buy, price) + return result.result_volume diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/log_price_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/log_price_example.py new file mode 100644 index 0000000..405df72 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/log_price_example.py @@ -0,0 +1,19 @@ +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class LogPricesExample(ScriptStrategyBase): + """ + This example shows how to get the ask and bid of a market and log it to the console. + """ + markets = { + "binance_paper_trade": {"ETH-USDT"}, + "kucoin_paper_trade": {"ETH-USDT"}, + "gate_io_paper_trade": {"ETH-USDT"} + } + + def on_tick(self): + for connector_name, connector in self.connectors.items(): + self.logger().info(f"Connector: {connector_name}") + self.logger().info(f"Best ask: {connector.get_price('ETH-USDT', True)}") + self.logger().info(f"Best bid: {connector.get_price('ETH-USDT', False)}") + self.logger().info(f"Mid price: {connector.get_mid_price('ETH-USDT')}") diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/macd_bb_directional_strategy.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/macd_bb_directional_strategy.py new file mode 100644 index 0000000..965cf53 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/macd_bb_directional_strategy.py @@ -0,0 +1,235 @@ +import datetime +import os +from collections import deque +from decimal import Decimal +from typing import Deque, Dict, List + +import pandas as pd +import pandas_ta as ta # noqa: F401 + +from hummingbot import data_path +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.core.data_type.common import OrderType, PositionAction, PositionMode, PositionSide +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.smart_components.position_executor.data_types import PositionConfig +from hummingbot.smart_components.position_executor.position_executor import PositionExecutor +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class MACDBBDirectionalStrategy(ScriptStrategyBase): + """ + A simple trading strategy that uses RSI in one timeframe to determine whether to go long or short. + IMPORTANT: Binance perpetual has to be in Single Asset Mode, soon we are going to support Multi Asset Mode. + """ + # Define the trading pair and exchange that we want to use and the csv where we are going to store the entries + trading_pair = "APE-BUSD" + exchange = "binance_perpetual" + + # Maximum position executors at a time + max_executors = 1 + active_executors: List[PositionExecutor] = [] + stored_executors: Deque[PositionExecutor] = deque(maxlen=10) # Store only the last 10 executors for reporting + + # Configure the parameters for the position + stop_loss_multiplier = 0.75 + take_profit_multiplier = 1.5 + time_limit = 60 * 55 + + # Create the candles that we want to use and the thresholds for the indicators + # IMPORTANT: The connector name of the candles can be binance or binance_perpetual, and can be different from the + # connector that you define to trade + candles = CandlesFactory.get_candle(connector="binance_perpetual", + trading_pair=trading_pair, + interval="3m", max_records=150) + + # Configure the leverage and order amount the bot is going to use + set_leverage_flag = None + leverage = 20 + order_amount_usd = Decimal("15") + + today = datetime.datetime.today() + csv_path = data_path() + f"/{exchange}_{trading_pair}_{today.day:02d}-{today.month:02d}-{today.year}.csv" + markets = {exchange: {trading_pair}} + + def __init__(self, connectors: Dict[str, ConnectorBase]): + # Is necessary to start the Candles Feed. + super().__init__(connectors) + self.candles.start() + + def get_active_executors(self): + return [signal_executor for signal_executor in self.active_executors + if not signal_executor.is_closed] + + def get_closed_executors(self): + return self.stored_executors + + def on_tick(self): + self.check_and_set_leverage() + if len(self.get_active_executors()) < self.max_executors and self.candles.is_ready: + signal_value, take_profit, stop_loss, indicators = self.get_signal_tp_and_sl() + if self.is_margin_enough() and signal_value != 0: + price = self.connectors[self.exchange].get_mid_price(self.trading_pair) + self.notify_hb_app_with_timestamp(f""" + Creating new position! + Price: {price} + BB%: {indicators[0]} + MACDh: {indicators[1]} + MACD: {indicators[2]} + """) + signal_executor = PositionExecutor( + position_config=PositionConfig( + timestamp=self.current_timestamp, trading_pair=self.trading_pair, + exchange=self.exchange, order_type=OrderType.MARKET, + side=PositionSide.SHORT if signal_value < 0 else PositionSide.LONG, + entry_price=price, + amount=self.order_amount_usd / price, + stop_loss=stop_loss, + take_profit=take_profit, + time_limit=self.time_limit), + strategy=self, + ) + self.active_executors.append(signal_executor) + self.clean_and_store_executors() + + def get_signal_tp_and_sl(self): + candles_df = self.candles.candles_df + # Let's add some technical indicators + candles_df.ta.bbands(length=100, append=True) + candles_df.ta.macd(fast=21, slow=42, signal=9, append=True) + candles_df["std"] = candles_df["close"].rolling(100).std() + candles_df["std_close"] = candles_df["std"] / candles_df["close"] + last_candle = candles_df.iloc[-1] + bbp = last_candle["BBP_100_2.0"] + macdh = last_candle["MACDh_21_42_9"] + macd = last_candle["MACD_21_42_9"] + std_pct = last_candle["std_close"] + if bbp < 0.2 and macdh > 0 and macd < 0: + signal_value = 1 + elif bbp > 0.8 and macdh < 0 and macd > 0: + signal_value = -1 + else: + signal_value = 0 + take_profit = std_pct * self.take_profit_multiplier + stop_loss = std_pct * self.stop_loss_multiplier + indicators = [bbp, macdh, macd] + return signal_value, take_profit, stop_loss, indicators + + def on_stop(self): + """ + Without this functionality, the network iterator will continue running forever after stopping the strategy + That's why is necessary to introduce this new feature to make a custom stop with the strategy. + """ + # we are going to close all the open positions when the bot stops + self.close_open_positions() + self.candles.stop() + + def format_status(self) -> str: + """ + Displays the three candlesticks involved in the script with RSI, BBANDS and EMA. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + + if len(self.stored_executors) > 0: + lines.extend([ + "\n########################################## Closed Executors ##########################################"]) + + for executor in self.stored_executors: + lines.extend([f"|Signal id: {executor.timestamp}"]) + lines.extend(executor.to_format_status()) + lines.extend([ + "-----------------------------------------------------------------------------------------------------------"]) + + if len(self.active_executors) > 0: + lines.extend([ + "\n########################################## Active Executors ##########################################"]) + + for executor in self.active_executors: + lines.extend([f"|Signal id: {executor.timestamp}"]) + lines.extend(executor.to_format_status()) + if self.candles.is_ready: + lines.extend([ + "\n############################################ Market Data ############################################\n"]) + signal, take_profit, stop_loss, indicators = self.get_signal_tp_and_sl() + lines.extend([f"Signal: {signal} | Take Profit: {take_profit} | Stop Loss: {stop_loss}"]) + lines.extend([f"BB%: {indicators[0]} | MACDh: {indicators[1]} | MACD: {indicators[2]}"]) + lines.extend(["\n-----------------------------------------------------------------------------------------------------------\n"]) + else: + lines.extend(["", " No data collected."]) + + return "\n".join(lines) + + def check_and_set_leverage(self): + if not self.set_leverage_flag: + for connector in self.connectors.values(): + for trading_pair in connector.trading_pairs: + connector.set_position_mode(PositionMode.HEDGE) + connector.set_leverage(trading_pair=trading_pair, leverage=self.leverage) + self.set_leverage_flag = True + + def clean_and_store_executors(self): + executors_to_store = [executor for executor in self.active_executors if executor.is_closed] + if not os.path.exists(self.csv_path): + df_header = pd.DataFrame([("timestamp", + "exchange", + "trading_pair", + "side", + "amount", + "pnl", + "close_timestamp", + "entry_price", + "close_price", + "last_status", + "sl", + "tp", + "tl", + "order_type", + "leverage")]) + df_header.to_csv(self.csv_path, mode='a', header=False, index=False) + for executor in executors_to_store: + self.stored_executors.append(executor) + df = pd.DataFrame([(executor.timestamp, + executor.exchange, + executor.trading_pair, + executor.side, + executor.amount, + executor.trade_pnl, + executor.close_timestamp, + executor.entry_price, + executor.close_price, + executor.status, + executor.position_config.stop_loss, + executor.position_config.take_profit, + executor.position_config.time_limit, + executor.open_order_type, + self.leverage)]) + df.to_csv(self.csv_path, mode='a', header=False, index=False) + self.active_executors = [executor for executor in self.active_executors if not executor.is_closed] + + def close_open_positions(self): + # we are going to close all the open positions when the bot stops + for connector_name, connector in self.connectors.items(): + for trading_pair, position in connector.account_positions.items(): + if position.position_side == PositionSide.LONG: + self.sell(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) + elif position.position_side == PositionSide.SHORT: + self.buy(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) + + def is_margin_enough(self): + quote_balance = self.connectors[self.exchange].get_available_balance(self.trading_pair.split("-")[-1]) + if self.order_amount_usd < quote_balance * self.leverage: + return True + else: + self.logger().info("No enough margin to place orders.") + return False diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/market_buy.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/market_buy.py new file mode 100644 index 0000000..2883b04 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/market_buy.py @@ -0,0 +1,18 @@ +from decimal import Decimal + +from hummingbot.core.data_type.common import OrderType +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class MarketBuyExample(ScriptStrategyBase): + order_amount = Decimal("0.001") + buy_executed = False + exchange = "mexc" + trading_pair = "BTC-USDT" + markets = {exchange: {trading_pair}} + + def on_tick(self): + if not self.buy_executed: + self.buy_executed = True + self.buy(connector_name=self.exchange, trading_pair=self.trading_pair, amount=self.order_amount, + order_type=OrderType.MARKET) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/microprice_calculator.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/microprice_calculator.py new file mode 100644 index 0000000..bf1e65e --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/microprice_calculator.py @@ -0,0 +1,305 @@ +import datetime +import os +from decimal import Decimal +from operator import itemgetter + +import numpy as np +import pandas as pd +from scipy.linalg import block_diag + +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class MicropricePMM(ScriptStrategyBase): + # ! Configuration + trading_pair = "ETH-USDT" + exchange = "kucoin_paper_trade" + range_of_imbalance = 1 # ? Compute imbalance from [best bid/ask, +/- ticksize*range_of_imbalance) + + # ! Microprice configuration + dt = 1 + n_imb = 6 # ? Needs to be large enough to capture shape of imbalance adjustmnts without being too large to capture noise + + # ! Advanced configuration variables + show_data = False # ? Controls whether current df is shown in status + path_to_data = './data' # ? Default file format './data/microprice_{trading_pair}_{exchange}_{date}.csv' + interval_to_write = 60 + price_line_width = 60 + precision = 4 # ? should be the length of the ticksize + data_size_min = 10000 # ? Seems to be the ideal value to get microprice adjustment values for other spreads + day_offset = 1 # ? How many days back to start looking for csv files to load data from + + # ! Script variabes + columns = ['date', 'time', 'bid', 'bs', 'ask', 'as'] + current_dataframe = pd.DataFrame(columns=columns) + time_to_write = 0 + markets = {exchange: {trading_pair}} + g_star = None + recording_data = True + ticksize = None + n_spread = None + + # ! System methods + def on_tick(self): + # Record data, dump data, update write timestamp + self.record_data() + if self.time_to_write < self.current_timestamp: + self.time_to_write = self.interval_to_write + self.current_timestamp + self.dump_data() + + def format_status(self) -> str: + bid, ask = itemgetter('bid', 'ask')(self.get_bid_ask()) + bar = '=' * self.price_line_width + '\n' + header = f'Trading pair: {self.trading_pair}\nExchange: {self.exchange}\n' + price_line = f'Adjusted Midprice: {self.compute_adjusted_midprice()}\n Midprice: {round((bid + ask) / 2, 8)}\n = {round(self.compute_adjusted_midprice() - ((bid + ask) / 2), 20)}\n\n{self.get_price_line()}\n' + imbalance_line = f'Imbalance: {self.compute_imbalance()}\n{self.get_imbalance_line()}\n' + data = f'Data path: {self.get_csv_path()}\n' + g_star = f'g_star:\n{self.g_star}' if self.g_star is not None else '' + + return f"\n\n\n{bar}\n\n{header}\n{price_line}\n\n{imbalance_line}\nn_spread: {self.n_spread} {'tick' if self.n_spread == 1 else 'ticks'}\n\n\n{g_star}\n\n{data}\n\n{bar}\n\n\n" + + # ! Data recording methods + # Records a new row to the dataframe every tick + # Every 'time_to_write' ticks, writes the dataframe to a csv file + def record_data(self): + # Fetch bid and ask data + bid, ask, bid_volume, ask_volume = itemgetter('bid', 'ask', 'bs', 'as')(self.get_bid_ask()) + # Fetch date and time in seconds + date = datetime.datetime.now().strftime("%Y-%m-%d") + time = self.current_timestamp + + data = [[date, time, bid, bid_volume, ask, ask_volume]] + self.current_dataframe = self.current_dataframe.append(pd.DataFrame(data, columns=self.columns), ignore_index=True) + return + + def dump_data(self): + if len(self.current_dataframe) < 2 * self.range_of_imbalance: + return + # Dump data to csv file + csv_path = f'{self.path_to_data}/microprice_{self.trading_pair}_{self.exchange}_{datetime.datetime.now().strftime("%Y-%m-%d")}.csv' + try: + data = pd.read_csv(csv_path, index_col=[0]) + except Exception as e: + self.logger().info(e) + self.logger().info(f'Creating new csv file at {csv_path}') + data = pd.DataFrame(columns=self.columns) + + data = data.append(self.current_dataframe.iloc[:-self.range_of_imbalance], ignore_index=True) + data.to_csv(csv_path) + self.current_dataframe = self.current_dataframe.iloc[-self.range_of_imbalance:] + return + +# ! Data methods + def get_csv_path(self): + # Get all files in self.path_to_data directory + files = os.listdir(self.path_to_data) + for i in files: + if i.startswith(f'microprice_{self.trading_pair}_{self.exchange}'): + len_data = len(pd.read_csv(f'{self.path_to_data}/{i}', index_col=[0])) + if len_data > self.data_size_min: + return f'{self.path_to_data}/{i}' + + # Otherwise just return today's file + return f'{self.path_to_data}/microprice_{self.trading_pair}_{self.exchange}_{datetime.datetime.now().strftime("%Y-%m-%d")}.csv' + + def get_bid_ask(self): + bids, asks = self.connectors[self.exchange].get_order_book(self.trading_pair).snapshot + # if size > 0, return average of range + best_ask = asks.iloc[0].price + ask_volume = asks.iloc[0].amount + best_bid = bids.iloc[0].price + bid_volume = bids.iloc[0].amount + return {'bid': best_bid, 'ask': best_ask, 'bs': bid_volume, 'as': ask_volume} + + # ! Microprice methods + def compute_adjusted_midprice(self): + data = self.get_df() + if len(data) < self.data_size_min or self.current_dataframe.empty: + self.recording_data = True + return -1 + if self.n_spread is None: + self.n_spread = self.compute_n_spread() + if self.g_star is None: + ticksize, g_star = self.compute_G_star(data) + self.g_star = g_star + self.ticksize = ticksize + # Compute adjusted midprice from G_star and mid + bid, ask = itemgetter('bid', 'ask')(self.get_bid_ask()) + mid = (bid + ask) / 2 + G_star = self.g_star + ticksize = self.ticksize + n_spread = self.n_spread + + # ? Compute adjusted midprice + last_row = self.current_dataframe.iloc[-1] + imb = last_row['bs'].astype(float) / (last_row['bs'].astype(float) + last_row['as'].astype(float)) + # Compute bucket of imbalance + imb_bucket = [abs(x - imb) for x in G_star.columns].index(min([abs(x - imb) for x in G_star.columns])) + # Compute and round spread index to nearest ticksize + spreads = G_star[G_star.columns[imb_bucket]].values + spread = last_row['ask'].astype(float) - last_row['bid'].astype(float) + # ? Generally we expect this value to be < self._n_spread so we log when it's > self._n_spread + spread_bucket = round(spread / ticksize) * ticksize // ticksize - 1 + if spread_bucket >= n_spread: + spread_bucket = n_spread - 1 + spread_bucket = int(spread_bucket) + # Compute adjusted midprice + adj_midprice = mid + spreads[spread_bucket] + return round(adj_midprice, self.precision * 2) + + def compute_G_star(self, data): + n_spread = self.n_spread + T, ticksize = self.prep_data_sym(data, self.n_imb, self.dt, n_spread) + imb = np.linspace(0, 1, self.n_imb) + G1, B = self.estimate(T, n_spread, self.n_imb) + # Calculate G1 then B^6*G1 + G2 = np.dot(B, G1) + G1 + G3 = G2 + np.dot(np.dot(B, B), G1) + G4 = G3 + np.dot(np.dot(np.dot(B, B), B), G1) + G5 = G4 + np.dot(np.dot(np.dot(np.dot(B, B), B), B), G1) + G6 = G5 + np.dot(np.dot(np.dot(np.dot(np.dot(B, B), B), B), B), G1) + # Reorganize G6 into buckets + index = [str(i + 1) for i in range(0, n_spread)] + G_star = pd.DataFrame(G6.reshape(n_spread, self.n_imb), index=index, columns=imb) + return ticksize, G_star + + def G_star_invalid(self, G_star, ticksize): + # Check if any values of G_star > ticksize/2 + if np.any(G_star > ticksize / 2): + return True + # Check if any values of G_star < -ticksize/2 + if np.any(G_star < -ticksize / 2): + return True + # Round middle values of G_star to self.precision and check if any values are 0 + if np.any(np.round(G_star.iloc[int(self.n_imb / 2)], self.precision) == 0): + return True + return False + + def estimate(self, T, n_spread, n_imb): + no_move = T[T['dM'] == 0] + no_move_counts = no_move.pivot_table(index=['next_imb_bucket'], + columns=['spread', 'imb_bucket'], + values='time', + fill_value=0, + aggfunc='count').unstack() + Q_counts = np.resize(np.array(no_move_counts[0:(n_imb * n_imb)]), (n_imb, n_imb)) + # loop over all spreads and add block matrices + for i in range(1, n_spread): + Qi = np.resize(np.array(no_move_counts[(i * n_imb * n_imb):(i + 1) * (n_imb * n_imb)]), (n_imb, n_imb)) + Q_counts = block_diag(Q_counts, Qi) + move_counts = T[(T['dM'] != 0)].pivot_table(index=['dM'], + columns=['spread', 'imb_bucket'], + values='time', + fill_value=0, + aggfunc='count').unstack() + + R_counts = np.resize(np.array(move_counts), (n_imb * n_spread, 4)) + T1 = np.concatenate((Q_counts, R_counts), axis=1).astype(float) + for i in range(0, n_imb * n_spread): + T1[i] = T1[i] / T1[i].sum() + Q = T1[:, 0:(n_imb * n_spread)] + R1 = T1[:, (n_imb * n_spread):] + + K = np.array([-0.01, -0.005, 0.005, 0.01]) + move_counts = T[(T['dM'] != 0)].pivot_table(index=['spread', 'imb_bucket'], + columns=['next_spread', 'next_imb_bucket'], + values='time', + fill_value=0, + aggfunc='count') + + R2_counts = np.resize(np.array(move_counts), (n_imb * n_spread, n_imb * n_spread)) + T2 = np.concatenate((Q_counts, R2_counts), axis=1).astype(float) + + for i in range(0, n_imb * n_spread): + T2[i] = T2[i] / T2[i].sum() + R2 = T2[:, (n_imb * n_spread):] + G1 = np.dot(np.dot(np.linalg.inv(np.eye(n_imb * n_spread) - Q), R1), K) + B = np.dot(np.linalg.inv(np.eye(n_imb * n_spread) - Q), R2) + return G1, B + + def compute_n_spread(self, T=None): + if not T: + T = self.get_df() + spread = T.ask - T.bid + spread_counts = spread.value_counts() + return len(spread_counts[spread_counts > self.data_size_min]) + + def prep_data_sym(self, T, n_imb, dt, n_spread): + spread = T.ask - T.bid + ticksize = np.round(min(spread.loc[spread > 0]) * 100) / 100 + # T.spread=T.ask-T.bid + # adds the spread and mid prices + T['spread'] = np.round((T['ask'] - T['bid']) / ticksize) * ticksize + T['mid'] = (T['bid'] + T['ask']) / 2 + # filter out spreads >= n_spread + T = T.loc[(T.spread <= n_spread * ticksize) & (T.spread > 0)] + T['imb'] = T['bs'] / (T['bs'] + T['as']) + # discretize imbalance into percentiles + T['imb_bucket'] = pd.qcut(T['imb'], n_imb, labels=False, duplicates='drop') + T['next_mid'] = T['mid'].shift(-dt) + # step ahead state variables + T['next_spread'] = T['spread'].shift(-dt) + T['next_time'] = T['time'].shift(-dt) + T['next_imb_bucket'] = T['imb_bucket'].shift(-dt) + # step ahead change in price + T['dM'] = np.round((T['next_mid'] - T['mid']) / ticksize * 2) * ticksize / 2 + T = T.loc[(T.dM <= ticksize * 1.1) & (T.dM >= -ticksize * 1.1)] + # symetrize data + T2 = T.copy(deep=True) + T2['imb_bucket'] = n_imb - 1 - T2['imb_bucket'] + T2['next_imb_bucket'] = n_imb - 1 - T2['next_imb_bucket'] + T2['dM'] = -T2['dM'] + T2['mid'] = -T2['mid'] + T3 = pd.concat([T, T2]) + T3.index = pd.RangeIndex(len(T3.index)) + return T3, ticksize + + def get_df(self): + csv_path = self.get_csv_path() + try: + df = pd.read_csv(csv_path, index_col=[0]) + df = df.append(self.current_dataframe) + except Exception as e: + self.logger().info(e) + df = self.current_dataframe + + df['time'] = df['time'].astype(float) + df['bid'] = df['bid'].astype(float) + df['ask'] = df['ask'].astype(float) + df['bs'] = df['bs'].astype(float) + df['as'] = df['as'].astype(float) + df['mid'] = (df['bid'] + df['ask']) / float(2) + df['imb'] = df['bs'] / (df['bs'] + df['as']) + return df + + def compute_imbalance(self) -> Decimal: + if self.get_df().empty or self.current_dataframe.empty: + self.logger().info('No data to compute imbalance, recording data') + self.recording_data = True + return Decimal(-1) + bid_size = self.current_dataframe['bs'].sum() + ask_size = self.current_dataframe['as'].sum() + return round(Decimal(bid_size) / Decimal(bid_size + ask_size), self.precision * 2) + + # ! Format status methods + def get_price_line(self) -> str: + # Get best bid and ask + bid, ask = itemgetter('bid', 'ask')(self.get_bid_ask()) + # Mid price is center of line + price_line = int(self.price_line_width / 2) * '-' + '|' + int(self.price_line_width / 2) * '-' + # Add bid, adjusted midprice, + bid_offset = int(self.price_line_width / 2 - len(str(bid)) - (len(str(self.compute_adjusted_midprice())) / 2)) + ask_offset = int(self.price_line_width / 2 - len(str(ask)) - (len(str(self.compute_adjusted_midprice())) / 2)) + labels = str(bid) + bid_offset * ' ' + str(self.compute_adjusted_midprice()) + ask_offset * ' ' + str(ask) + '\n' + # Create microprice of size 'price_line_width' with ends best bid and ask + mid = (bid + ask) / 2 + spread = ask - bid + microprice_adjustment = self.compute_adjusted_midprice() - mid + (spread / 2) + adjusted_midprice_i = int(microprice_adjustment / spread * self.price_line_width) + 1 + price_line = price_line[:adjusted_midprice_i] + 'm' + price_line[adjusted_midprice_i:] + return labels + price_line + + def get_imbalance_line(self) -> str: + imb_line = int(self.price_line_width / 2) * '-' + '|' + int(self.price_line_width / 2) * '-' + imb_line = imb_line[:int(self.compute_imbalance() * self.price_line_width)] + 'i' + imb_line[int(self.compute_imbalance() * self.price_line_width):] + return imb_line diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/pmm_with_position_executor.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/pmm_with_position_executor.py new file mode 100644 index 0000000..f2305d3 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/pmm_with_position_executor.py @@ -0,0 +1,342 @@ +import datetime +import os +import time +from decimal import Decimal +from typing import Dict, List + +import pandas as pd + +from hummingbot import data_path +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.core.data_type.common import OrderType, PositionAction, PositionMode, PositionSide, PriceType, TradeType +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.smart_components.position_executor.data_types import ( + CloseType, + PositionConfig, + PositionExecutorStatus, + TrailingStop, +) +from hummingbot.smart_components.position_executor.position_executor import PositionExecutor +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class PMMWithPositionExecutor(ScriptStrategyBase): + """ + BotCamp Cohort: Sept 2022 + Design Template: https://hummingbot-foundation.notion.site/Simple-PMM-63cc765486dd42228d3da0b32537fc92 + Video: - + Description: + The bot will place two orders around the price_source (mid price or last traded price) in a trading_pair on + exchange, with a distance defined by the ask_spread and bid_spread. Every order_refresh_time in seconds, + the bot will cancel and replace the orders. + """ + market_making_strategy_name = "pmm_with_position_executor" + trading_pair = "FRONT-BUSD" + exchange = "binance" + + # Configure order levels and spreads + order_levels = { + 1: {"spread_factor": 1.7, "order_amount_usd": Decimal("13")}, + 2: {"spread_factor": 3.4, "order_amount_usd": Decimal("21")}, + } + position_mode: PositionMode = PositionMode.HEDGE + active_executors: List[PositionExecutor] = [] + stored_executors: List[PositionExecutor] = [] + + # Configure the parameters for the position + stop_loss: float = 0.03 + take_profit: float = 0.015 + time_limit: int = 3600 * 24 + executor_refresh_time: int = 30 + open_order_type = OrderType.LIMIT + take_profit_order_type: OrderType = OrderType.MARKET + stop_loss_order_type: OrderType = OrderType.MARKET + time_limit_order_type: OrderType = OrderType.MARKET + trailing_stop_activation_delta = 0.003 + trailing_stop_trailing_delta = 0.001 + # Here you can use for example the LastTrade price to use in your strategy + price_source = PriceType.MidPrice + candles = [CandlesFactory.get_candle(connector=exchange, + trading_pair=trading_pair, + interval="15m") + ] + + # Configure the leverage and order amount the bot is going to use + set_leverage_flag = None + leverage = 1 + inventory_balance_pct = Decimal("0.4") + inventory_balance_tol = Decimal("0.05") + _inventory_balanced = False + spreads = None + reference_price = None + + markets = {exchange: {trading_pair}} + + @property + def is_perpetual(self): + """ + Checks if the exchange is a perpetual market. + """ + return "perpetual" in self.exchange + + def get_csv_path(self) -> str: + today = datetime.datetime.today() + csv_path = data_path() + f"/{self.market_making_strategy_name}_position_executors_{self.exchange}_{self.trading_pair}_{today.day:02d}-{today.month:02d}-{today.year}.csv" + return csv_path + + @property + def all_candles_ready(self): + """ + Checks if the candlesticks are full. + """ + return all([candle.is_ready for candle in self.candles]) + + def get_active_executors(self): + return [signal_executor for signal_executor in self.active_executors + if not signal_executor.is_closed] + + def __init__(self, connectors: Dict[str, ConnectorBase]): + # Is necessary to start the Candles Feed. + super().__init__(connectors) + for candle in self.candles: + candle.start() + self._active_bids = {level: None for level in self.order_levels.keys()} + self._active_asks = {level: None for level in self.order_levels.keys()} + + def on_stop(self): + """ + Without this functionality, the network iterator will continue running forever after stopping the strategy + That's why is necessary to introduce this new feature to make a custom stop with the strategy. + """ + if self.is_perpetual: + # we are going to close all the open positions when the bot stops + self.close_open_positions() + else: + self.check_and_rebalance_inventory() + for candle in self.candles: + candle.stop() + + def on_tick(self): + if self.is_perpetual: + self.check_and_set_leverage() + elif not self._inventory_balanced: + self.check_and_rebalance_inventory() + if self.all_candles_ready: + self.update_parameters() + self.check_and_create_executors() + self.clean_and_store_executors() + + def update_parameters(self): + candles_df = self.get_candles_with_features() + natr = candles_df["NATR_21"].iloc[-1] + bbp = candles_df["BBP_200_2.0"].iloc[-1] + price_multiplier = ((0.5 - bbp) / 0.5) * natr * 0.3 + price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source) + self.spreads = natr + self.reference_price = price * Decimal(str(1 + price_multiplier)) + + def get_candles_with_features(self): + candles_df = self.candles[0].candles_df + candles_df.ta.bbands(length=200, append=True) + candles_df.ta.natr(length=21, scalar=2, append=True) + return candles_df + + def create_executor(self, side: TradeType, price: Decimal, amount_usd: Decimal): + position_config = PositionConfig( + timestamp=self.current_timestamp, + trading_pair=self.trading_pair, + exchange=self.exchange, + side=side, + amount=amount_usd / price, + take_profit=self.take_profit, + stop_loss=self.stop_loss, + time_limit=self.time_limit, + entry_price=price, + open_order_type=self.open_order_type, + take_profit_order_type=self.take_profit_order_type, + stop_loss_order_type=self.stop_loss_order_type, + time_limit_order_type=self.time_limit_order_type, + trailing_stop=TrailingStop( + activation_price_delta=self.trailing_stop_activation_delta, + trailing_delta=self.trailing_stop_trailing_delta + ), + leverage=self.leverage, + ) + executor = PositionExecutor( + strategy=self, + position_config=position_config, + ) + return executor + + def check_and_set_leverage(self): + if not self.set_leverage_flag: + for connector in self.connectors.values(): + for trading_pair in connector.trading_pairs: + connector.set_position_mode(self.position_mode) + connector.set_leverage(trading_pair=trading_pair, leverage=self.leverage) + self.set_leverage_flag = True + + def clean_and_store_executors(self): + executors_to_store = [] + for level, executor in self._active_bids.items(): + if executor: + age = time.time() - executor.position_config.timestamp + if age > self.executor_refresh_time and executor.executor_status == PositionExecutorStatus.NOT_STARTED: + executor.early_stop() + if executor.is_closed: + executors_to_store.append(executor) + self._active_bids[level] = None + for level, executor in self._active_asks.items(): + if executor: + age = time.time() - executor.position_config.timestamp + if age > self.executor_refresh_time and executor.executor_status == PositionExecutorStatus.NOT_STARTED: + executor.early_stop() + if executor.is_closed: + executors_to_store.append(executor) + self._active_asks[level] = None + + csv_path = self.get_csv_path() + if not os.path.exists(csv_path): + df_header = pd.DataFrame([("timestamp", + "exchange", + "trading_pair", + "side", + "amount", + "trade_pnl", + "trade_pnl_quote", + "cum_fee_quote", + "net_pnl_quote", + "net_pnl", + "close_timestamp", + "executor_status", + "close_type", + "entry_price", + "close_price", + "sl", + "tp", + "tl", + "open_order_type", + "take_profit_order_type", + "stop_loss_order_type", + "time_limit_order_type", + "leverage" + )]) + df_header.to_csv(csv_path, mode='a', header=False, index=False) + for executor in executors_to_store: + self.stored_executors.append(executor) + df = pd.DataFrame([(executor.position_config.timestamp, + executor.exchange, + executor.trading_pair, + executor.side, + executor.amount, + executor.trade_pnl, + executor.trade_pnl_quote, + executor.cum_fee_quote, + executor.net_pnl_quote, + executor.net_pnl, + executor.close_timestamp, + executor.executor_status, + executor.close_type, + executor.entry_price, + executor.close_price, + executor.position_config.stop_loss, + executor.position_config.take_profit, + executor.position_config.time_limit, + executor.open_order_type, + executor.take_profit_order_type, + executor.stop_loss_order_type, + executor.time_limit_order_type, + self.leverage)]) + df.to_csv(self.get_csv_path(), mode='a', header=False, index=False) + + def close_open_positions(self): + # we are going to close all the open positions when the bot stops + for connector_name, connector in self.connectors.items(): + for trading_pair, position in connector.account_positions.items(): + if position.position_side == PositionSide.LONG: + self.sell(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) + elif position.position_side == PositionSide.SHORT: + self.buy(connector_name=connector_name, + trading_pair=position.trading_pair, + amount=abs(position.amount), + order_type=OrderType.MARKET, + price=connector.get_mid_price(position.trading_pair), + position_action=PositionAction.CLOSE) + + def market_data_extra_info(self): + return ["\n"] + + def format_status(self) -> str: + """ + Displays the three candlesticks involved in the script with RSI, BBANDS and EMA. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + + if len(self.stored_executors) > 0: + lines.extend(["\n################################## Closed Executors ##################################"]) + for executor in [executor for executor in self.stored_executors if executor.close_type not in [CloseType.EXPIRED, CloseType.INSUFFICIENT_BALANCE]]: + lines.extend([f"|Signal id: {executor.position_config.timestamp}"]) + lines.extend(executor.to_format_status()) + lines.extend([ + "-----------------------------------------------------------------------------------------------------------"]) + + lines.extend(["\n################################## Active Bids ##################################"]) + for level, executor in self._active_bids.items(): + if executor: + lines.extend([f"|Signal id: {executor.position_config.timestamp}"]) + lines.extend(executor.to_format_status()) + lines.extend([ + "-----------------------------------------------------------------------------------------------------------"]) + lines.extend(["\n################################## Active Asks ##################################"]) + for level, executor in self._active_asks.items(): + if executor: + lines.extend([f"|Signal id: {executor.position_config.timestamp}"]) + lines.extend(executor.to_format_status()) + lines.extend([ + "-----------------------------------------------------------------------------------------------------------"]) + if self.all_candles_ready: + lines.extend(["\n################################## Market Data ##################################\n"]) + lines.extend(self.market_data_extra_info()) + else: + lines.extend(["", " No data collected."]) + + return "\n".join(lines) + + def check_and_rebalance_inventory(self): + base_balance = self.connectors[self.exchange].get_available_balance(self.trading_pair.split("-")[0]) + quote_balance = self.connectors[self.exchange].get_available_balance(self.trading_pair.split("-")[1]) + price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source) + total_balance = base_balance + quote_balance / price + balance_ratio = base_balance / total_balance + if abs(balance_ratio - self.inventory_balance_pct) < self.inventory_balance_tol: + self._inventory_balanced = True + return + base_target_balance = total_balance * Decimal(self.inventory_balance_pct) + base_delta = base_target_balance - base_balance + if base_delta > 0: + self.buy(self.exchange, self.trading_pair, base_delta, OrderType.MARKET, price) + elif base_delta < 0: + self.sell(self.exchange, self.trading_pair, base_delta, OrderType.MARKET, price) + self._inventory_balanced = True + + def check_and_create_executors(self): + for level, executor in self._active_asks.items(): + if executor is None: + level_config = self.order_levels[level] + price = self.reference_price * Decimal(1 + self.spreads * level_config["spread_factor"]) + executor = self.create_executor(side=TradeType.SELL, price=price, amount_usd=level_config["order_amount_usd"]) + self._active_asks[level] = executor + + for level, executor in self._active_bids.items(): + if executor is None: + level_config = self.order_levels[level] + price = self.reference_price * Decimal(1 - self.spreads * level_config["spread_factor"]) + executor = self.create_executor(side=TradeType.BUY, price=price, amount_usd=level_config["order_amount_usd"]) + self._active_bids[level] = executor diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/pmm_with_shifted_mid_dynamic_spreads.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/pmm_with_shifted_mid_dynamic_spreads.py new file mode 100644 index 0000000..020fb4c --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/pmm_with_shifted_mid_dynamic_spreads.py @@ -0,0 +1,174 @@ +import logging +from decimal import Decimal +from typing import Dict, List + +import pandas_ta as ta # noqa: F401 + +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.core.data_type.common import OrderType, PriceType, TradeType +from hummingbot.core.data_type.order_candidate import OrderCandidate +from hummingbot.core.event.events import BuyOrderCompletedEvent, OrderFilledEvent, SellOrderCompletedEvent +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class PMMhShiftedMidPriceDynamicSpread(ScriptStrategyBase): + """ + Design Template: https://hummingbot-foundation.notion.site/Simple-PMM-with-shifted-mid-price-and-dynamic-spreads-63cc765486dd42228d3da0b32537fc92 + Video: - + Description: + The bot will place two orders around the `reference_price` (mid price or last traded price +- %based on `RSI` value ) + in a `trading_pair` on `exchange`, with a distance defined by the `spread` multiplied by `spreads_factors` + based on `NATR`. Every `order_refresh_time` seconds, the bot will cancel and replace the orders. + """ + # Define the variables that we are going to use for the spreads + # We are going to divide the NATR by the spread_base to get the spread_multiplier + # If NATR = 0.002 = 0.2% --> the spread_factor will be 0.002 / 0.008 = 0.25 + # Formula: spread_multiplier = NATR / spread_base + spread_base = 0.008 + spread_multiplier = 1 + + # Define the price source and the multiplier that shifts the price + # We are going to use the max price shift in percentage as the middle of the NATR + # If NATR = 0.002 = 0.2% --> the maximum shift from the mid-price is 0.2%, and that will be calculated with RSI + # If RSI = 100 --> it will shift the mid-price -0.2% and if RSI = 0 --> it will shift the mid-price +0.2% + # Formula: price_multiplier = ((50 - RSI) / 50)) * NATR + price_source = PriceType.MidPrice + price_multiplier = 1 + + # Trading conf + order_refresh_time = 15 + order_amount = 7 + trading_pair = "RLC-USDT" + exchange = "binance" + + # Creating instance of the candles + candles = CandlesFactory.get_candle(connector=exchange, + trading_pair=trading_pair, + interval="3m") + + # Variables to store the volume and quantity of orders + + total_sell_orders = 0 + total_buy_orders = 0 + total_sell_volume = 0 + total_buy_volume = 0 + create_timestamp = 0 + + markets = {exchange: {trading_pair}} + + def __init__(self, connectors: Dict[str, ConnectorBase]): + # Is necessary to start the Candles Feed. + super().__init__(connectors) + self.candles.start() + + def on_stop(self): + """ + Without this functionality, the network iterator will continue running forever after stopping the strategy + That's why is necessary to introduce this new feature to make a custom stop with the strategy. + """ + # we are going to close all the open positions when the bot stops + self.candles.stop() + + def on_tick(self): + if self.create_timestamp <= self.current_timestamp and self.candles.is_ready: + self.cancel_all_orders() + self.update_multipliers() + proposal: List[OrderCandidate] = self.create_proposal() + proposal_adjusted: List[OrderCandidate] = self.adjust_proposal_to_budget(proposal) + self.place_orders(proposal_adjusted) + self.create_timestamp = self.order_refresh_time + self.current_timestamp + + def get_candles_with_features(self): + candles_df = self.candles.candles_df + candles_df.ta.rsi(length=14, append=True) + candles_df.ta.natr(length=14, scalar=0.5, append=True) + return candles_df + + def update_multipliers(self): + candles_df = self.get_candles_with_features() + self.price_multiplier = ((50 - candles_df["RSI_14"].iloc[-1]) / 50) * (candles_df["NATR_14"].iloc[-1]) + self.spread_multiplier = candles_df["NATR_14"].iloc[-1] / self.spread_base + + def create_proposal(self) -> List[OrderCandidate]: + mid_price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source) + reference_price = mid_price * Decimal(str(1 + self.price_multiplier)) + spreads_adjusted = self.spread_multiplier * self.spread_base + buy_price = reference_price * Decimal(1 - spreads_adjusted) + sell_price = reference_price * Decimal(1 + spreads_adjusted) + + buy_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT, + order_side=TradeType.BUY, amount=Decimal(self.order_amount), price=buy_price) + + sell_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT, + order_side=TradeType.SELL, amount=Decimal(self.order_amount), price=sell_price) + + return [buy_order, sell_order] + + def adjust_proposal_to_budget(self, proposal: List[OrderCandidate]) -> List[OrderCandidate]: + proposal_adjusted = self.connectors[self.exchange].budget_checker.adjust_candidates(proposal, all_or_none=True) + return proposal_adjusted + + def place_orders(self, proposal: List[OrderCandidate]) -> None: + for order in proposal: + if order.amount != 0: + self.place_order(connector_name=self.exchange, order=order) + else: + self.logger().info(f"Not enough funds to place the {order.order_type} order") + + def place_order(self, connector_name: str, order: OrderCandidate): + if order.order_side == TradeType.SELL: + self.sell(connector_name=connector_name, trading_pair=order.trading_pair, amount=order.amount, + order_type=order.order_type, price=order.price) + elif order.order_side == TradeType.BUY: + self.buy(connector_name=connector_name, trading_pair=order.trading_pair, amount=order.amount, + order_type=order.order_type, price=order.price) + + def cancel_all_orders(self): + for order in self.get_active_orders(connector_name=self.exchange): + self.cancel(self.exchange, order.trading_pair, order.client_order_id) + + def did_fill_order(self, event: OrderFilledEvent): + msg = ( + f"{event.trade_type.name} {round(event.amount, 2)} {event.trading_pair} {self.exchange} at {round(event.price, 2)}") + self.log_with_clock(logging.INFO, msg) + self.total_buy_volume += event.amount if event.trade_type == TradeType.BUY else 0 + self.total_sell_volume += event.amount if event.trade_type == TradeType.SELL else 0 + + def did_complete_buy_order(self, event: BuyOrderCompletedEvent): + self.total_buy_orders += 1 + + def did_complete_sell_order(self, event: SellOrderCompletedEvent): + self.total_sell_orders += 1 + + def format_status(self) -> str: + """ + Returns status of the current strategy on user balances and current active orders. This function is called + when status command is issued. Override this function to create custom status display output. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + + balance_df = self.get_balance_df() + lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")]) + + try: + df = self.active_orders_df() + lines.extend(["", " Orders:"] + [" " + line for line in df.to_string(index=False).split("\n")]) + except ValueError: + lines.extend(["", " No active maker orders."]) + mid_price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source) + reference_price = mid_price * Decimal(str(1 + self.price_multiplier)) + lines.extend(["\n-----------------------------------------------------------------------------------------------------------\n"]) + lines.extend(["", f" Total Buy Orders: {self.total_buy_orders:.2f} | Total Sell Orders: {self.total_sell_orders:.2f}"]) + lines.extend(["", f" Total Buy Volume: {self.total_buy_volume:.2f} | Total Sell Volume: {self.total_sell_volume:.2f}"]) + lines.extend(["\n-----------------------------------------------------------------------------------------------------------\n"]) + lines.extend(["", f" Spread Base: {self.spread_base:.4f} | Spread Adjusted: {(self.spread_multiplier * self.spread_base):.4f} | Spread Multiplier: {self.spread_multiplier:.4f}"]) + lines.extend(["", f" Mid Price: {mid_price:.4f} | Price shifted: {reference_price:.4f} | Price Multiplier: {self.price_multiplier:.4f}"]) + lines.extend(["\n-----------------------------------------------------------------------------------------------------------\n"]) + candles_df = self.get_candles_with_features() + lines.extend([f"Candles: {self.candles.name} | Interval: {self.candles.interval}"]) + lines.extend([" " + line for line in candles_df.tail().to_string(index=False).split("\n")]) + lines.extend(["\n-----------------------------------------------------------------------------------------------------------\n"]) + return "\n".join(lines) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_arbitrage_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_arbitrage_example.py new file mode 100644 index 0000000..fb478ab --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_arbitrage_example.py @@ -0,0 +1,193 @@ +import logging +from decimal import Decimal +from typing import Any, Dict + +import pandas as pd + +from hummingbot.core.data_type.common import OrderType, TradeType +from hummingbot.core.data_type.order_candidate import OrderCandidate +from hummingbot.core.event.events import OrderFilledEvent +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class SimpleArbitrage(ScriptStrategyBase): + """ + BotCamp Cohort: Sept 2022 + Design Template: https://hummingbot-foundation.notion.site/Simple-Arbitrage-51b2af6e54b6493dab12e5d537798c07 + Video: TBD + Description: + A simplified version of Hummingbot arbitrage strategy, this bot checks the Volume Weighted Average Price for + bid and ask in two exchanges and if it finds a profitable opportunity, it will trade the tokens. + """ + order_amount = Decimal("0.01") # in base asset + min_profitability = Decimal("0.002") # in percentage + base = "ETH" + quote = "USDT" + trading_pair = f"{base}-{quote}" + exchange_A = "binance_paper_trade" + exchange_B = "kucoin_paper_trade" + + markets = {exchange_A: {trading_pair}, + exchange_B: {trading_pair}} + + def on_tick(self): + vwap_prices = self.get_vwap_prices_for_amount(self.order_amount) + proposal = self.check_profitability_and_create_proposal(vwap_prices) + if len(proposal) > 0: + proposal_adjusted: Dict[str, OrderCandidate] = self.adjust_proposal_to_budget(proposal) + self.place_orders(proposal_adjusted) + + def get_vwap_prices_for_amount(self, amount: Decimal): + bid_ex_a = self.connectors[self.exchange_A].get_vwap_for_volume(self.trading_pair, False, amount) + ask_ex_a = self.connectors[self.exchange_A].get_vwap_for_volume(self.trading_pair, True, amount) + bid_ex_b = self.connectors[self.exchange_B].get_vwap_for_volume(self.trading_pair, False, amount) + ask_ex_b = self.connectors[self.exchange_B].get_vwap_for_volume(self.trading_pair, True, amount) + vwap_prices = { + self.exchange_A: { + "bid": bid_ex_a.result_price, + "ask": ask_ex_a.result_price + }, + self.exchange_B: { + "bid": bid_ex_b.result_price, + "ask": ask_ex_b.result_price + } + } + return vwap_prices + + def get_fees_percentages(self, vwap_prices: Dict[str, Any]) -> Dict: + # We assume that the fee percentage for buying or selling is the same + a_fee = self.connectors[self.exchange_A].get_fee( + base_currency=self.base, + quote_currency=self.quote, + order_type=OrderType.MARKET, + order_side=TradeType.BUY, + amount=self.order_amount, + price=vwap_prices[self.exchange_A]["ask"], + is_maker=False + ).percent + + b_fee = self.connectors[self.exchange_B].get_fee( + base_currency=self.base, + quote_currency=self.quote, + order_type=OrderType.MARKET, + order_side=TradeType.BUY, + amount=self.order_amount, + price=vwap_prices[self.exchange_B]["ask"], + is_maker=False + ).percent + + return { + self.exchange_A: a_fee, + self.exchange_B: b_fee + } + + def get_profitability_analysis(self, vwap_prices: Dict[str, Any]) -> Dict: + fees = self.get_fees_percentages(vwap_prices) + buy_a_sell_b_quote = vwap_prices[self.exchange_A]["ask"] * (1 - fees[self.exchange_A]) * self.order_amount - \ + vwap_prices[self.exchange_B]["bid"] * (1 + fees[self.exchange_B]) * self.order_amount + buy_a_sell_b_base = buy_a_sell_b_quote / ( + (vwap_prices[self.exchange_A]["ask"] + vwap_prices[self.exchange_B]["bid"]) / 2) + + buy_b_sell_a_quote = vwap_prices[self.exchange_B]["ask"] * (1 - fees[self.exchange_B]) * self.order_amount - \ + vwap_prices[self.exchange_A]["bid"] * (1 + fees[self.exchange_A]) * self.order_amount + + buy_b_sell_a_base = buy_b_sell_a_quote / ( + (vwap_prices[self.exchange_B]["ask"] + vwap_prices[self.exchange_A]["bid"]) / 2) + + return { + "buy_a_sell_b": + { + "quote_diff": buy_a_sell_b_quote, + "base_diff": buy_a_sell_b_base, + "profitability_pct": buy_a_sell_b_base / self.order_amount + }, + "buy_b_sell_a": + { + "quote_diff": buy_b_sell_a_quote, + "base_diff": buy_b_sell_a_base, + "profitability_pct": buy_b_sell_a_base / self.order_amount + }, + } + + def check_profitability_and_create_proposal(self, vwap_prices: Dict[str, Any]) -> Dict: + proposal = {} + profitability_analysis = self.get_profitability_analysis(vwap_prices) + if profitability_analysis["buy_a_sell_b"]["profitability_pct"] > self.min_profitability: + # This means that the ask of the first exchange is lower than the bid of the second one + proposal[self.exchange_A] = OrderCandidate(trading_pair=self.trading_pair, is_maker=False, + order_type=OrderType.MARKET, + order_side=TradeType.BUY, amount=self.order_amount, + price=vwap_prices[self.exchange_A]["ask"]) + proposal[self.exchange_B] = OrderCandidate(trading_pair=self.trading_pair, is_maker=False, + order_type=OrderType.MARKET, + order_side=TradeType.SELL, amount=Decimal(self.order_amount), + price=vwap_prices[self.exchange_B]["bid"]) + elif profitability_analysis["buy_b_sell_a"]["profitability_pct"] > self.min_profitability: + # This means that the ask of the second exchange is lower than the bid of the first one + proposal[self.exchange_B] = OrderCandidate(trading_pair=self.trading_pair, is_maker=False, + order_type=OrderType.MARKET, + order_side=TradeType.BUY, amount=self.order_amount, + price=vwap_prices[self.exchange_B]["ask"]) + proposal[self.exchange_A] = OrderCandidate(trading_pair=self.trading_pair, is_maker=False, + order_type=OrderType.MARKET, + order_side=TradeType.SELL, amount=Decimal(self.order_amount), + price=vwap_prices[self.exchange_A]["bid"]) + + return proposal + + def adjust_proposal_to_budget(self, proposal: Dict[str, OrderCandidate]) -> Dict[str, OrderCandidate]: + for connector, order in proposal.items(): + proposal[connector] = self.connectors[connector].budget_checker.adjust_candidate(order, all_or_none=True) + return proposal + + def place_orders(self, proposal: Dict[str, OrderCandidate]) -> None: + for connector, order in proposal.items(): + self.place_order(connector_name=connector, order=order) + + def place_order(self, connector_name: str, order: OrderCandidate): + if order.order_side == TradeType.SELL: + self.sell(connector_name=connector_name, trading_pair=order.trading_pair, amount=order.amount, + order_type=order.order_type, price=order.price) + elif order.order_side == TradeType.BUY: + self.buy(connector_name=connector_name, trading_pair=order.trading_pair, amount=order.amount, + order_type=order.order_type, price=order.price) + + def format_status(self) -> str: + """ + Returns status of the current strategy on user balances and current active orders. This function is called + when status command is issued. Override this function to create custom status display output. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + warning_lines = [] + warning_lines.extend(self.network_warning(self.get_market_trading_pair_tuples())) + + balance_df = self.get_balance_df() + lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")]) + + vwap_prices = self.get_vwap_prices_for_amount(self.order_amount) + lines.extend(["", " VWAP Prices for amount"] + [" " + line for line in + pd.DataFrame(vwap_prices).to_string().split("\n")]) + profitability_analysis = self.get_profitability_analysis(vwap_prices) + lines.extend(["", " Profitability (%)"] + [ + f" Buy A: {self.exchange_A} --> Sell B: {self.exchange_B}"] + [ + f" Quote Diff: {profitability_analysis['buy_a_sell_b']['quote_diff']:.7f}"] + [ + f" Base Diff: {profitability_analysis['buy_a_sell_b']['base_diff']:.7f}"] + [ + f" Percentage: {profitability_analysis['buy_a_sell_b']['profitability_pct'] * 100:.4f} %"] + [ + f" Buy B: {self.exchange_B} --> Sell A: {self.exchange_A}"] + [ + f" Quote Diff: {profitability_analysis['buy_b_sell_a']['quote_diff']:.7f}"] + [ + f" Base Diff: {profitability_analysis['buy_b_sell_a']['base_diff']:.7f}"] + [ + f" Percentage: {profitability_analysis['buy_b_sell_a']['profitability_pct'] * 100:.4f} %" + ]) + + warning_lines.extend(self.balance_warning(self.get_market_trading_pair_tuples())) + if len(warning_lines) > 0: + lines.extend(["", "*** WARNINGS ***"] + warning_lines) + return "\n".join(lines) + + def did_fill_order(self, event: OrderFilledEvent): + msg = ( + f"{event.trade_type.name} {round(event.amount, 2)} {event.trading_pair} at {round(event.price, 2)}") + self.log_with_clock(logging.INFO, msg) + self.notify_hb_app_with_timestamp(msg) \ No newline at end of file diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_pmm_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_pmm_example.py new file mode 100644 index 0000000..8370819 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_pmm_example.py @@ -0,0 +1,77 @@ +import logging +from decimal import Decimal +from typing import List + +from hummingbot.core.data_type.common import OrderType, PriceType, TradeType +from hummingbot.core.data_type.order_candidate import OrderCandidate +from hummingbot.core.event.events import OrderFilledEvent +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class SimplePMM(ScriptStrategyBase): + """ + BotCamp Cohort: Sept 2022 + Design Template: https://hummingbot-foundation.notion.site/Simple-PMM-63cc765486dd42228d3da0b32537fc92 + Video: - + Description: + The bot will place two orders around the price_source (mid price or last traded price) in a trading_pair on + exchange, with a distance defined by the ask_spread and bid_spread. Every order_refresh_time in seconds, + the bot will cancel and replace the orders. + """ + bid_spread = 0.0001 + ask_spread = 0.0001 + order_refresh_time = 15 + order_amount = 0.01 + create_timestamp = 0 + trading_pair = "ETH-USDT" + exchange = "binance_paper_trade" + # Here you can use for example the LastTrade price to use in your strategy + price_source = PriceType.MidPrice + + markets = {exchange: {trading_pair}} + + def on_tick(self): + if self.create_timestamp <= self.current_timestamp: + self.cancel_all_orders() + proposal: List[OrderCandidate] = self.create_proposal() + proposal_adjusted: List[OrderCandidate] = self.adjust_proposal_to_budget(proposal) + self.place_orders(proposal_adjusted) + self.create_timestamp = self.order_refresh_time + self.current_timestamp + + def create_proposal(self) -> List[OrderCandidate]: + ref_price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source) + buy_price = ref_price * Decimal(1 - self.bid_spread) + sell_price = ref_price * Decimal(1 + self.ask_spread) + + buy_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT, + order_side=TradeType.BUY, amount=Decimal(self.order_amount), price=buy_price) + + sell_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT, + order_side=TradeType.SELL, amount=Decimal(self.order_amount), price=sell_price) + + return [buy_order, sell_order] + + def adjust_proposal_to_budget(self, proposal: List[OrderCandidate]) -> List[OrderCandidate]: + proposal_adjusted = self.connectors[self.exchange].budget_checker.adjust_candidates(proposal, all_or_none=True) + return proposal_adjusted + + def place_orders(self, proposal: List[OrderCandidate]) -> None: + for order in proposal: + self.place_order(connector_name=self.exchange, order=order) + + def place_order(self, connector_name: str, order: OrderCandidate): + if order.order_side == TradeType.SELL: + self.sell(connector_name=connector_name, trading_pair=order.trading_pair, amount=order.amount, + order_type=order.order_type, price=order.price) + elif order.order_side == TradeType.BUY: + self.buy(connector_name=connector_name, trading_pair=order.trading_pair, amount=order.amount, + order_type=order.order_type, price=order.price) + + def cancel_all_orders(self): + for order in self.get_active_orders(connector_name=self.exchange): + self.cancel(self.exchange, order.trading_pair, order.client_order_id) + + def did_fill_order(self, event: OrderFilledEvent): + msg = (f"{event.trade_type.name} {round(event.amount, 2)} {event.trading_pair} {self.exchange} at {round(event.price, 2)}") + self.log_with_clock(logging.INFO, msg) + self.notify_hb_app_with_timestamp(msg) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_rsi_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_rsi_example.py new file mode 100644 index 0000000..7f8fa59 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_rsi_example.py @@ -0,0 +1,259 @@ +import math +import os +from decimal import Decimal +from typing import Optional + +import pandas as pd + +from hummingbot.client.hummingbot_application import HummingbotApplication +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.connector.utils import combine_to_hb_trading_pair +from hummingbot.core.data_type.common import OrderType, TradeType +from hummingbot.core.data_type.order_candidate import OrderCandidate +from hummingbot.core.event.event_forwarder import SourceInfoEventForwarder +from hummingbot.core.event.events import OrderBookEvent, OrderBookTradeEvent, OrderFilledEvent +from hummingbot.core.rate_oracle.rate_oracle import RateOracle +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class SimpleRSIScript(ScriptStrategyBase): + """ + The strategy is to buy on overbought signal and sell on oversold. + """ + connector_name = os.getenv("CONNECTOR_NAME", "binance_paper_trade") + base = os.getenv("BASE", "BTC") + quote = os.getenv("QUOTE", "USDT") + timeframe = os.getenv("TIMEFRAME", "1s") + + position_amount_usd = Decimal(os.getenv("POSITION_AMOUNT_USD", "50")) + + rsi_length = int(os.getenv("RSI_LENGTH", "14")) + + # If true - uses Exponential Moving Average, if false - Simple Moving Average. + rsi_is_ema = os.getenv("RSI_IS_EMA", 'True').lower() in ('true', '1', 't') + + buy_rsi = int(os.getenv("BUY_RSI", "30")) + sell_rsi = int(os.getenv("SELL_RSI", "70")) + + # It depends on a timeframe. Make sure you have enough trades to calculate rsi_length number of candlesticks. + trade_count_limit = int(os.getenv("TRADE_COUNT_LIMIT", "100000")) + + trading_pair = combine_to_hb_trading_pair(base, quote) + markets = {connector_name: {trading_pair}} + + subscribed_to_order_book_trade_event: bool = False + position: Optional[OrderFilledEvent] = None + + _trades: 'list[OrderBookTradeEvent]' = [] + _cumulative_price_change_pct = Decimal(0) + _filling_position: bool = False + + def on_tick(self): + """ + On every tick calculate OHLCV candlesticks, calculate RSI, react on overbought or oversold signal with creating, + adjusting and sending an order. + """ + if not self.subscribed_to_order_book_trade_event: + # Set pandas resample rule for a timeframe + self._set_resample_rule(self.timeframe) + self.subscribe_to_order_book_trade_event() + elif len(self._trades) > 0: + df = self.calculate_candlesticks() + df = self.calculate_rsi(df, self.rsi_length, self.rsi_is_ema) + should_open_position = self.should_open_position(df) + should_close_position = self.should_close_position(df) + if should_open_position or should_close_position: + order_side = TradeType.BUY if should_open_position else TradeType.SELL + order_candidate = self.create_order_candidate(order_side) + # Adjust OrderCandidate + order_adjusted = self.connectors[self.connector_name].budget_checker.adjust_candidate(order_candidate, all_or_none=False) + if math.isclose(order_adjusted.amount, Decimal("0"), rel_tol=1E-5): + self.logger().info(f"Order adjusted: {order_adjusted.amount}, too low to place an order") + else: + self.send_order(order_adjusted) + else: + self._rsi = df.iloc[-1]['rsi'] + self.logger().info(f"RSI is {self._rsi:.0f}") + + def _set_resample_rule(self, timeframe): + """ + Convert timeframe to pandas resample rule value. + https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.resample.html + """ + timeframe_to_rule = { + "1s": "1S", + "10s": "10S", + "30s": "30S", + "1m": "1T", + "15m": "15T" + } + if timeframe not in timeframe_to_rule.keys(): + self.logger().error(f"{timeframe} timeframe is not mapped to resample rule.") + HummingbotApplication.main_application().stop() + self._resample_rule = timeframe_to_rule[timeframe] + + def should_open_position(self, df: pd.DataFrame) -> bool: + """ + If overbought and not in the position. + """ + rsi: float = df.iloc[-1]['rsi'] + rsi_is_calculated = pd.notna(rsi) + time_to_buy = rsi_is_calculated and rsi <= self.buy_rsi + can_buy = self.position is None and not self._filling_position + return can_buy and time_to_buy + + def should_close_position(self, df: pd.DataFrame) -> bool: + """ + If oversold and in the position. + """ + rsi: float = df.iloc[-1]['rsi'] + rsi_is_calculated = pd.notna(rsi) + time_to_sell = rsi_is_calculated and rsi >= self.sell_rsi + can_sell = self.position is not None and not self._filling_position + return can_sell and time_to_sell + + def create_order_candidate(self, order_side: bool) -> OrderCandidate: + """ + Create and quantize order candidate. + """ + connector: ConnectorBase = self.connectors[self.connector_name] + is_buy = order_side == TradeType.BUY + price = connector.get_price(self.trading_pair, is_buy) + if is_buy: + conversion_rate = RateOracle.get_instance().get_pair_rate(self.trading_pair) + amount = self.position_amount_usd / conversion_rate + else: + amount = self.position.amount + + amount = connector.quantize_order_amount(self.trading_pair, amount) + price = connector.quantize_order_price(self.trading_pair, price) + return OrderCandidate( + trading_pair=self.trading_pair, + is_maker = False, + order_type = OrderType.LIMIT, + order_side = order_side, + amount = amount, + price = price) + + def send_order(self, order: OrderCandidate): + """ + Send order to the exchange, indicate that position is filling, and send log message with a trade. + """ + is_buy = order.order_side == TradeType.BUY + place_order = self.buy if is_buy else self.sell + place_order( + connector_name=self.connector_name, + trading_pair=self.trading_pair, + amount=order.amount, + order_type=order.order_type, + price=order.price + ) + self._filling_position = True + if is_buy: + msg = f"RSI is below {self.buy_rsi:.2f}, buying {order.amount:.5f} {self.base} with limit order at {order.price:.2f} ." + else: + msg = (f"RSI is above {self.sell_rsi:.2f}, selling {self.position.amount:.5f} {self.base}" + f" with limit order at ~ {order.price:.2f}, entry price was {self.position.price:.2f}.") + self.notify_hb_app_with_timestamp(msg) + self.logger().info(msg) + + def calculate_candlesticks(self) -> pd.DataFrame: + """ + Convert raw trades to OHLCV dataframe. + """ + df = pd.DataFrame(self._trades) + df['timestamp'] = pd.to_datetime(df['timestamp'], unit='s') + df["timestamp"] = pd.to_datetime(df["timestamp"]) + df.drop(columns=[df.columns[0]], axis=1, inplace=True) + df = df.set_index('timestamp') + df = df.resample(self._resample_rule).agg({ + 'price': ['first', 'max', 'min', 'last'], + 'amount': 'sum', + }) + df.columns = df.columns.to_flat_index().map(lambda x: x[1]) + df.rename(columns={'first': 'open', 'max': 'high', 'min': 'low', 'last': 'close', 'sum': 'volume'}, inplace=True) + return df + + def did_fill_order(self, event: OrderFilledEvent): + """ + Indicate that position is filled, save position properties on enter, calculate cumulative price change on exit. + """ + if event.trade_type == TradeType.BUY: + self.position = event + self._filling_position = False + elif event.trade_type == TradeType.SELL: + delta_price = (event.price - self.position.price) / self.position.price + self._cumulative_price_change_pct += delta_price + self.position = None + self._filling_position = False + else: + self.logger().warn(f"Unsupported order type filled: {event.trade_type}") + + @staticmethod + def calculate_rsi(df: pd.DataFrame, length: int = 14, is_ema: bool = True): + """ + Calculate relative strength index and add it to the dataframe. + """ + close_delta = df['close'].diff() + up = close_delta.clip(lower=0) + down = close_delta.clip(upper=0).abs() + + if is_ema: + # Exponential Moving Average + ma_up = up.ewm(com = length - 1, adjust=True, min_periods = length).mean() + ma_down = down.ewm(com = length - 1, adjust=True, min_periods = length).mean() + else: + # Simple Moving Average + ma_up = up.rolling(window = length, adjust=False).mean() + ma_down = down.rolling(window = length, adjust=False).mean() + + rs = ma_up / ma_down + df["rsi"] = 100 - (100 / (1 + rs)) + return df + + def subscribe_to_order_book_trade_event(self): + """ + Subscribe to raw trade event. + """ + self.order_book_trade_event = SourceInfoEventForwarder(self._process_public_trade) + for market in self.connectors.values(): + for order_book in market.order_books.values(): + order_book.add_listener(OrderBookEvent.TradeEvent, self.order_book_trade_event) + self.subscribed_to_order_book_trade_event = True + + def _process_public_trade(self, event_tag: int, market: ConnectorBase, event: OrderBookTradeEvent): + """ + Add new trade to list, remove old trade event, if count greater than trade_count_limit. + """ + if len(self._trades) >= self.trade_count_limit: + self._trades.pop(0) + self._trades.append(event) + + def format_status(self) -> str: + """ + Returns status of the current strategy on user balances and current active orders. This function is called + when status command is issued. Override this function to create custom status display output. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + warning_lines = [] + warning_lines.extend(self.network_warning(self.get_market_trading_pair_tuples())) + + balance_df = self.get_balance_df() + lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")]) + + try: + df = self.active_orders_df() + lines.extend(["", " Orders:"] + [" " + line for line in df.to_string(index=False).split("\n")]) + except ValueError: + lines.extend(["", " No active maker orders."]) + + # Strategy specific info + lines.extend(["", " Current RSI:"] + [" " + f"{self._rsi:.0f}"]) + lines.extend(["", " Simple RSI strategy total price change with all trades:"] + [" " + f"{self._cumulative_price_change_pct:.5f}" + " %"]) + + warning_lines.extend(self.balance_warning(self.get_market_trading_pair_tuples())) + if len(warning_lines) > 0: + lines.extend(["", "*** WARNINGS ***"] + warning_lines) + return "\n".join(lines) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_vwap_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_vwap_example.py new file mode 100644 index 0000000..89dcf08 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_vwap_example.py @@ -0,0 +1,184 @@ +import logging +import math +from decimal import Decimal +from typing import Dict + +from hummingbot.connector.utils import split_hb_trading_pair +from hummingbot.core.data_type.order_candidate import OrderCandidate +from hummingbot.core.event.events import OrderFilledEvent, OrderType, TradeType +from hummingbot.core.rate_oracle.rate_oracle import RateOracle +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class VWAPExample(ScriptStrategyBase): + """ + BotCamp Cohort: Sept 2022 + Design Template: https://hummingbot-foundation.notion.site/Simple-VWAP-Example-d43a929cc5bd45c6b1a72f63e6635618 + Video: - + Description: + This example lets you create one VWAP in a market using a percentage of the sum volume of the order book + until a spread from the mid price. + This example demonstrates: + - How to get the account balance + - How to get the bids and asks of a market + - How to code a "utility" strategy + """ + last_ordered_ts = 0 + vwap: Dict = {"connector_name": "binance_paper_trade", "trading_pair": "ETH-USDT", "is_buy": True, + "total_volume_usd": 100000, "price_spread": 0.001, "volume_perc": 0.001, "order_delay_time": 10} + markets = {vwap["connector_name"]: {vwap["trading_pair"]}} + + def on_tick(self): + """ + Every order delay time the strategy will buy or sell the base asset. It will compute the cumulative order book + volume until the spread and buy a percentage of that. + The input of the strategy is in USD, but we will use the rate oracle to get a target base that will be static. + - Use the Rate Oracle to get a conversion rate + - Create proposal (a list of order candidates) + - Check the account balance and adjust the proposal accordingly (lower order amount if needed) + - Lastly, execute the proposal on the exchange + """ + if self.last_ordered_ts < (self.current_timestamp - self.vwap["order_delay_time"]): + if self.vwap.get("status") is None: + self.init_vwap_stats() + elif self.vwap.get("status") == "ACTIVE": + vwap_order: OrderCandidate = self.create_order() + vwap_order_adjusted = self.vwap["connector"].budget_checker.adjust_candidate(vwap_order, + all_or_none=False) + if math.isclose(vwap_order_adjusted.amount, Decimal("0"), rel_tol=1E-5): + self.logger().info(f"Order adjusted: {vwap_order_adjusted.amount}, too low to place an order") + else: + self.place_order( + connector_name=self.vwap["connector_name"], + trading_pair=self.vwap["trading_pair"], + is_buy=self.vwap["is_buy"], + amount=vwap_order_adjusted.amount, + order_type=vwap_order_adjusted.order_type) + self.last_ordered_ts = self.current_timestamp + + def init_vwap_stats(self): + # General parameters + vwap = self.vwap.copy() + vwap["connector"] = self.connectors[vwap["connector_name"]] + vwap["delta"] = 0 + vwap["trades"] = [] + vwap["status"] = "ACTIVE" + vwap["trade_type"] = TradeType.BUY if self.vwap["is_buy"] else TradeType.SELL + base_asset, quote_asset = split_hb_trading_pair(vwap["trading_pair"]) + + # USD conversion to quote and base asset + conversion_base_asset = f"{base_asset}-USD" + conversion_quote_asset = f"{quote_asset}-USD" + base_conversion_rate = RateOracle.get_instance().get_pair_rate(conversion_base_asset) + quote_conversion_rate = RateOracle.get_instance().get_pair_rate(conversion_quote_asset) + vwap["start_price"] = vwap["connector"].get_price(vwap["trading_pair"], vwap["is_buy"]) + vwap["target_base_volume"] = vwap["total_volume_usd"] / base_conversion_rate + vwap["ideal_quote_volume"] = vwap["total_volume_usd"] / quote_conversion_rate + + # Compute market order scenario + orderbook_query = vwap["connector"].get_quote_volume_for_base_amount(vwap["trading_pair"], vwap["is_buy"], + vwap["target_base_volume"]) + vwap["market_order_base_volume"] = orderbook_query.query_volume + vwap["market_order_quote_volume"] = orderbook_query.result_volume + vwap["volume_remaining"] = vwap["target_base_volume"] + vwap["real_quote_volume"] = Decimal(0) + self.vwap = vwap + + def create_order(self) -> OrderCandidate: + """ + Retrieves the cumulative volume of the order book until the price spread is reached, then takes a percentage + of that to use as order amount. + """ + # Compute the new price using the max spread allowed + mid_price = float(self.vwap["connector"].get_mid_price(self.vwap["trading_pair"])) + price_multiplier = 1 + self.vwap["price_spread"] if self.vwap["is_buy"] else 1 - self.vwap["price_spread"] + price_affected_by_spread = mid_price * price_multiplier + + # Query the cumulative volume until the price affected by spread + orderbook_query = self.vwap["connector"].get_volume_for_price( + trading_pair=self.vwap["trading_pair"], + is_buy=self.vwap["is_buy"], + price=price_affected_by_spread) + volume_for_price = orderbook_query.result_volume + + # Check if the volume available is higher than the remaining + amount = min(volume_for_price * Decimal(self.vwap["volume_perc"]), Decimal(self.vwap["volume_remaining"])) + + # Quantize the order amount and price + amount = self.vwap["connector"].quantize_order_amount(self.vwap["trading_pair"], amount) + price = self.vwap["connector"].quantize_order_price(self.vwap["trading_pair"], + Decimal(price_affected_by_spread)) + # Create the Order Candidate + vwap_order = OrderCandidate( + trading_pair=self.vwap["trading_pair"], + is_maker=False, + order_type=OrderType.MARKET, + order_side=self.vwap["trade_type"], + amount=amount, + price=price) + return vwap_order + + def place_order(self, + connector_name: str, + trading_pair: str, + is_buy: bool, + amount: Decimal, + order_type: OrderType, + price=Decimal("NaN"), + ): + if is_buy: + self.buy(connector_name, trading_pair, amount, order_type, price) + else: + self.sell(connector_name, trading_pair, amount, order_type, price) + + def did_fill_order(self, event: OrderFilledEvent): + """ + Listens to fill order event to log it and notify the Hummingbot application. + If you set up Telegram bot, you will get notification there as well. + """ + if event.trading_pair == self.vwap["trading_pair"] and event.trade_type == self.vwap["trade_type"]: + self.vwap["volume_remaining"] -= event.amount + self.vwap["delta"] = (self.vwap["target_base_volume"] - self.vwap["volume_remaining"]) / self.vwap[ + "target_base_volume"] + self.vwap["real_quote_volume"] += event.price * event.amount + self.vwap["trades"].append(event) + if math.isclose(self.vwap["delta"], 1, rel_tol=1e-5): + self.vwap["status"] = "COMPLETE" + msg = (f"({event.trading_pair}) {event.trade_type.name} order (price: {round(event.price, 2)}) of " + f"{round(event.amount, 2)} " + f"{split_hb_trading_pair(event.trading_pair)[0]} is filled.") + + self.log_with_clock(logging.INFO, msg) + self.notify_hb_app_with_timestamp(msg) + + def format_status(self) -> str: + """ + Returns status of the current strategy on user balances and current active orders. This function is called + when status command is issued. Override this function to create custom status display output. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + warning_lines = [] + warning_lines.extend(self.network_warning(self.get_market_trading_pair_tuples())) + + balance_df = self.get_balance_df() + lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")]) + + try: + df = self.active_orders_df() + lines.extend(["", " Orders:"] + [" " + line for line in df.to_string(index=False).split("\n")]) + except ValueError: + lines.extend(["", " No active maker orders."]) + lines.extend(["", "VWAP Info:"] + [" " + key + ": " + value + for key, value in self.vwap.items() + if type(value) == str]) + + lines.extend(["", "VWAP Stats:"] + [" " + key + ": " + str(round(value, 4)) + for key, value in self.vwap.items() + if type(value) in [int, float, Decimal]]) + + warning_lines.extend(self.balance_warning(self.get_market_trading_pair_tuples())) + if len(warning_lines) > 0: + lines.extend(["", "*** WARNINGS ***"] + warning_lines) + return "\n".join(lines) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_xemm_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_xemm_example.py new file mode 100644 index 0000000..3c8244b --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/simple_xemm_example.py @@ -0,0 +1,204 @@ +from decimal import Decimal + +import pandas as pd + +from hummingbot.core.data_type.common import OrderType, TradeType +from hummingbot.core.data_type.order_candidate import OrderCandidate +from hummingbot.core.event.events import OrderFilledEvent +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class SimpleXEMM(ScriptStrategyBase): + """ + BotCamp Cohort: Sept 2022 + Design Template: https://hummingbot-foundation.notion.site/Simple-XEMM-Example-f08cf7546ea94a44b389672fd21bb9ad + Video: https://www.loom.com/share/ca08fe7bc3d14ba68ae704305ac78a3a + Description: + A simplified version of Hummingbot cross-exchange market making strategy, this bot makes a market on + the maker pair and hedges any filled trades in the taker pair. If the spread (difference between maker order price + and taker hedge price) dips below min_spread, the bot refreshes the order + """ + + maker_exchange = "kucoin_paper_trade" + maker_pair = "ETH-USDT" + taker_exchange = "gate_io_paper_trade" + taker_pair = "ETH-USDT" + + order_amount = 0.1 # amount for each order + spread_bps = 10 # bot places maker orders at this spread to taker price + min_spread_bps = 0 # bot refreshes order if spread is lower than min-spread + slippage_buffer_spread_bps = 100 # buffer applied to limit taker hedging trades on taker exchange + max_order_age = 120 # bot refreshes orders after this age + + markets = {maker_exchange: {maker_pair}, taker_exchange: {taker_pair}} + + buy_order_placed = False + sell_order_placed = False + + def on_tick(self): + taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount) + taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount) + + if not self.buy_order_placed: + maker_buy_price = taker_sell_result.result_price * Decimal(1 - self.spread_bps / 10000) + buy_order_amount = min(self.order_amount, self.buy_hedging_budget()) + buy_order = OrderCandidate(trading_pair=self.maker_pair, is_maker=True, order_type=OrderType.LIMIT, order_side=TradeType.BUY, amount=Decimal(buy_order_amount), price=maker_buy_price) + buy_order_adjusted = self.connectors[self.maker_exchange].budget_checker.adjust_candidate(buy_order, all_or_none=False) + self.buy(self.maker_exchange, self.maker_pair, buy_order_adjusted.amount, buy_order_adjusted.order_type, buy_order_adjusted.price) + self.buy_order_placed = True + + if not self.sell_order_placed: + maker_sell_price = taker_buy_result.result_price * Decimal(1 + self.spread_bps / 10000) + sell_order_amount = min(self.order_amount, self.sell_hedging_budget()) + sell_order = OrderCandidate(trading_pair=self.maker_pair, is_maker=True, order_type=OrderType.LIMIT, order_side=TradeType.SELL, amount=Decimal(sell_order_amount), price=maker_sell_price) + sell_order_adjusted = self.connectors[self.maker_exchange].budget_checker.adjust_candidate(sell_order, all_or_none=False) + self.sell(self.maker_exchange, self.maker_pair, sell_order_adjusted.amount, sell_order_adjusted.order_type, sell_order_adjusted.price) + self.sell_order_placed = True + + for order in self.get_active_orders(connector_name=self.maker_exchange): + cancel_timestamp = order.creation_timestamp / 1000000 + self.max_order_age + if order.is_buy: + buy_cancel_threshold = taker_sell_result.result_price * Decimal(1 - self.min_spread_bps / 10000) + if order.price > buy_cancel_threshold or cancel_timestamp < self.current_timestamp: + self.logger().info(f"Cancelling buy order: {order.client_order_id}") + self.cancel(self.maker_exchange, order.trading_pair, order.client_order_id) + self.buy_order_placed = False + else: + sell_cancel_threshold = taker_buy_result.result_price * Decimal(1 + self.min_spread_bps / 10000) + if order.price < sell_cancel_threshold or cancel_timestamp < self.current_timestamp: + self.logger().info(f"Cancelling sell order: {order.client_order_id}") + self.cancel(self.maker_exchange, order.trading_pair, order.client_order_id) + self.sell_order_placed = False + return + + def buy_hedging_budget(self) -> Decimal: + balance = self.connectors[self.taker_exchange].get_available_balance("ETH") + return balance + + def sell_hedging_budget(self) -> Decimal: + balance = self.connectors[self.taker_exchange].get_available_balance("USDT") + taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount) + return balance / taker_buy_result.result_price + + def is_active_maker_order(self, event: OrderFilledEvent): + """ + Helper function that checks if order is an active order on the maker exchange + """ + for order in self.get_active_orders(connector_name=self.maker_exchange): + if order.client_order_id == event.order_id: + return True + return False + + def did_fill_order(self, event: OrderFilledEvent): + + mid_price = self.connectors[self.maker_exchange].get_mid_price(self.maker_pair) + if event.trade_type == TradeType.BUY and self.is_active_maker_order(event): + taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount) + sell_price_with_slippage = taker_sell_result.result_price * Decimal(1 - self.slippage_buffer_spread_bps / 10000) + self.logger().info(f"Filled maker buy order with price: {event.price}") + sell_spread_bps = (taker_sell_result.result_price - event.price) / mid_price * 10000 + self.logger().info(f"Sending taker sell order at price: {taker_sell_result.result_price} spread: {int(sell_spread_bps)} bps") + sell_order = OrderCandidate(trading_pair=self.taker_pair, is_maker=False, order_type=OrderType.LIMIT, order_side=TradeType.SELL, amount=Decimal(event.amount), price=sell_price_with_slippage) + sell_order_adjusted = self.connectors[self.taker_exchange].budget_checker.adjust_candidate(sell_order, all_or_none=False) + self.sell(self.taker_exchange, self.taker_pair, sell_order_adjusted.amount, sell_order_adjusted.order_type, sell_order_adjusted.price) + self.buy_order_placed = False + else: + if event.trade_type == TradeType.SELL and self.is_active_maker_order(event): + taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount) + buy_price_with_slippage = taker_buy_result.result_price * Decimal(1 + self.slippage_buffer_spread_bps / 10000) + buy_spread_bps = (event.price - taker_buy_result.result_price) / mid_price * 10000 + self.logger().info(f"Filled maker sell order at price: {event.price}") + self.logger().info(f"Sending taker buy order: {taker_buy_result.result_price} spread: {int(buy_spread_bps)}") + buy_order = OrderCandidate(trading_pair=self.taker_pair, is_maker=False, order_type=OrderType.LIMIT, order_side=TradeType.BUY, amount=Decimal(event.amount), price=buy_price_with_slippage) + buy_order_adjusted = self.connectors[self.taker_exchange].budget_checker.adjust_candidate(buy_order, all_or_none=False) + self.buy(self.taker_exchange, self.taker_pair, buy_order_adjusted.amount, buy_order_adjusted.order_type, buy_order_adjusted.price) + self.sell_order_placed = False + + def exchanges_df(self) -> pd.DataFrame: + """ + Return a custom data frame of prices on maker vs taker exchanges for display purposes + """ + mid_price = self.connectors[self.maker_exchange].get_mid_price(self.maker_pair) + maker_buy_result = self.connectors[self.maker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount) + maker_sell_result = self.connectors[self.maker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount) + taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount) + taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount) + maker_buy_spread_bps = (maker_buy_result.result_price - taker_buy_result.result_price) / mid_price * 10000 + maker_sell_spread_bps = (taker_sell_result.result_price - maker_sell_result.result_price) / mid_price * 10000 + columns = ["Exchange", "Market", "Mid Price", "Buy Price", "Sell Price", "Buy Spread", "Sell Spread"] + data = [] + data.append([ + self.maker_exchange, + self.maker_pair, + float(self.connectors[self.maker_exchange].get_mid_price(self.maker_pair)), + float(maker_buy_result.result_price), + float(maker_sell_result.result_price), + int(maker_buy_spread_bps), + int(maker_sell_spread_bps) + ]) + data.append([ + self.taker_exchange, + self.taker_pair, + float(self.connectors[self.taker_exchange].get_mid_price(self.maker_pair)), + float(taker_buy_result.result_price), + float(taker_sell_result.result_price), + int(-maker_buy_spread_bps), + int(-maker_sell_spread_bps) + ]) + df = pd.DataFrame(data=data, columns=columns) + return df + + def active_orders_df(self) -> pd.DataFrame: + """ + Returns a custom data frame of all active maker orders for display purposes + """ + columns = ["Exchange", "Market", "Side", "Price", "Amount", "Spread Mid", "Spread Cancel", "Age"] + data = [] + mid_price = self.connectors[self.maker_exchange].get_mid_price(self.maker_pair) + taker_buy_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, True, self.order_amount) + taker_sell_result = self.connectors[self.taker_exchange].get_price_for_volume(self.taker_pair, False, self.order_amount) + buy_cancel_threshold = taker_sell_result.result_price * Decimal(1 - self.min_spread_bps / 10000) + sell_cancel_threshold = taker_buy_result.result_price * Decimal(1 + self.min_spread_bps / 10000) + for connector_name, connector in self.connectors.items(): + for order in self.get_active_orders(connector_name): + age_txt = "n/a" if order.age() <= 0. else pd.Timestamp(order.age(), unit='s').strftime('%H:%M:%S') + spread_mid_bps = (mid_price - order.price) / mid_price * 10000 if order.is_buy else (order.price - mid_price) / mid_price * 10000 + spread_cancel_bps = (buy_cancel_threshold - order.price) / buy_cancel_threshold * 10000 if order.is_buy else (order.price - sell_cancel_threshold) / sell_cancel_threshold * 10000 + data.append([ + self.maker_exchange, + order.trading_pair, + "buy" if order.is_buy else "sell", + float(order.price), + float(order.quantity), + int(spread_mid_bps), + int(spread_cancel_bps), + age_txt + ]) + if not data: + raise ValueError + df = pd.DataFrame(data=data, columns=columns) + df.sort_values(by=["Market", "Side"], inplace=True) + return df + + def format_status(self) -> str: + """ + Returns status of the current strategy on user balances and current active orders. This function is called + when status command is issued. Override this function to create custom status display output. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + + balance_df = self.get_balance_df() + lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")]) + + exchanges_df = self.exchanges_df() + lines.extend(["", " Exchanges:"] + [" " + line for line in exchanges_df.to_string(index=False).split("\n")]) + + try: + orders_df = self.active_orders_df() + lines.extend(["", " Active Orders:"] + [" " + line for line in orders_df.to_string(index=False).split("\n")]) + except ValueError: + lines.extend(["", " No active maker orders."]) + + return "\n".join(lines) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/spot_perp_arb.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/spot_perp_arb.py new file mode 100644 index 0000000..65b4762 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/spot_perp_arb.py @@ -0,0 +1,492 @@ +from csv import writer as csv_writer +from datetime import datetime +from decimal import Decimal +from enum import Enum +from typing import Dict, List + +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.connector.utils import split_hb_trading_pair +from hummingbot.core.data_type.common import OrderType, PositionAction, PositionMode +from hummingbot.core.event.events import BuyOrderCompletedEvent, PositionModeChangeEvent, SellOrderCompletedEvent +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class StrategyState(Enum): + Closed = 0 # static state + Opening = 1 # in flight state + Opened = 2 # static state + Closing = 3 # in flight state + + +class StrategyAction(Enum): + NULL = 0 + BUY_SPOT_SHORT_PERP = 1 + SELL_SPOT_LONG_PERP = 2 + + +# TODO: handle corner cases -- spot price and perp price never cross again after position is opened +class SpotPerpArb(ScriptStrategyBase): + """ + PRECHECK: + 1. enough base and quote balance in spot (base is optional if you do one side only), enough quote balance in perp + 2. better to empty your position in perp + 3. check you have set one way mode (instead of hedge mode) in your futures account + + REFERENCE: hummingbot/strategy/spot_perpetual_arbitrage + """ + + spot_connector = "kucoin" + perp_connector = "kucoin_perpetual" + trading_pair = "HIGH-USDT" + markets = {spot_connector: {trading_pair}, perp_connector: {trading_pair}} + + leverage = 2 + is_position_mode_ready = False + + base_order_amount = Decimal("0.1") + buy_spot_short_perp_profit_margin_bps = 100 + sell_spot_long_perp_profit_margin_bps = 100 + # buffer to account for slippage when placing limit taker orders + slippage_buffer_bps = 15 + + strategy_state = StrategyState.Closed + last_strategy_action = StrategyAction.NULL + completed_order_ids = [] + next_arbitrage_opening_ts = 0 + next_arbitrage_opening_delay = 10 + in_flight_state_start_ts = 0 + in_flight_state_tolerance = 60 + opened_state_start_ts = 0 + opened_state_tolerance = 60 * 60 * 2 + + # write order book csv + order_book_csv = f"./data/spot_perp_arb_order_book_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.csv" + + def __init__(self, connectors: Dict[str, ConnectorBase]): + super().__init__(connectors) + self.set_leverage() + self.init_order_book_csv() + + def set_leverage(self) -> None: + perp_connector = self.connectors[self.perp_connector] + perp_connector.set_position_mode(PositionMode.ONEWAY) + perp_connector.set_leverage( + trading_pair=self.trading_pair, leverage=self.leverage + ) + self.logger().info( + f"Setting leverage to {self.leverage}x for {self.perp_connector} on {self.trading_pair}" + ) + + def init_order_book_csv(self) -> None: + self.logger().info("Preparing order book csv...") + with open(self.order_book_csv, "a") as f_object: + writer = csv_writer(f_object) + writer.writerow( + [ + "timestamp", + "spot_exchange", + "perp_exchange", + "spot_best_bid", + "spot_best_ask", + "perp_best_bid", + "perp_best_ask", + ] + ) + self.logger().info(f"Order book csv created: {self.order_book_csv}") + + def append_order_book_csv(self) -> None: + spot_best_bid_price = self.connectors[self.spot_connector].get_price( + self.trading_pair, False + ) + spot_best_ask_price = self.connectors[self.spot_connector].get_price( + self.trading_pair, True + ) + perp_best_bid_price = self.connectors[self.perp_connector].get_price( + self.trading_pair, False + ) + perp_best_ask_price = self.connectors[self.perp_connector].get_price( + self.trading_pair, True + ) + row = [ + str(self.current_timestamp), + self.spot_connector, + self.perp_connector, + str(spot_best_bid_price), + str(spot_best_ask_price), + str(perp_best_bid_price), + str(perp_best_ask_price), + ] + with open(self.order_book_csv, "a", newline="") as f_object: + writer = csv_writer(f_object) + writer.writerow(row) + self.logger().info(f"Order book csv updated: {self.order_book_csv}") + return + + def on_tick(self) -> None: + # precheck before running any trading logic + if not self.is_position_mode_ready: + return + + self.append_order_book_csv() + + # skip if orders are pending for completion + self.update_in_flight_state() + if self.strategy_state in (StrategyState.Opening, StrategyState.Closing): + if ( + self.current_timestamp + > self.in_flight_state_start_ts + self.in_flight_state_tolerance + ): + self.logger().warning( + "Orders has been submitted but not completed yet " + f"for more than {self.in_flight_state_tolerance} seconds. Please check your orders!" + ) + return + + # skip if its still in buffer time before next arbitrage opportunity + if ( + self.strategy_state == StrategyState.Closed + and self.current_timestamp < self.next_arbitrage_opening_ts + ): + return + + # flag out if position waits too long without any sign of closing + if ( + self.strategy_state == StrategyState.Opened + and self.current_timestamp + > self.opened_state_start_ts + self.opened_state_tolerance + ): + self.logger().warning( + f"Position has been opened for more than {self.opened_state_tolerance} seconds without any sign of closing. " + "Consider undoing the position manually or lower the profitability margin." + ) + + # TODO: change to async on order execution + # find opportunity and trade + if self.should_buy_spot_short_perp() and self.can_buy_spot_short_perp(): + self.update_static_state() + self.last_strategy_action = StrategyAction.BUY_SPOT_SHORT_PERP + self.buy_spot_short_perp() + elif self.should_sell_spot_long_perp() and self.can_sell_spot_long_perp(): + self.update_static_state() + self.last_strategy_action = StrategyAction.SELL_SPOT_LONG_PERP + self.sell_spot_long_perp() + + def update_in_flight_state(self) -> None: + if ( + self.strategy_state == StrategyState.Opening + and len(self.completed_order_ids) == 2 + ): + self.strategy_state = StrategyState.Opened + self.logger().info( + f"Position is opened with order_ids: {self.completed_order_ids}. " + "Changed the state from Opening to Opened." + ) + self.completed_order_ids.clear() + self.opened_state_start_ts = self.current_timestamp + elif ( + self.strategy_state == StrategyState.Closing + and len(self.completed_order_ids) == 2 + ): + self.strategy_state = StrategyState.Closed + self.next_arbitrage_opening_ts = ( + self.current_timestamp + self.next_arbitrage_opening_ts + ) + self.logger().info( + f"Position is closed with order_ids: {self.completed_order_ids}. " + "Changed the state from Closing to Closed.\n" + f"No arbitrage opportunity will be opened before {self.next_arbitrage_opening_ts}. " + f"(Current timestamp: {self.current_timestamp})" + ) + self.completed_order_ids.clear() + return + + def update_static_state(self) -> None: + if self.strategy_state == StrategyState.Closed: + self.strategy_state = StrategyState.Opening + self.logger().info("The state changed from Closed to Opening") + elif self.strategy_state == StrategyState.Opened: + self.strategy_state = StrategyState.Closing + self.logger().info("The state changed from Opened to Closing") + self.in_flight_state_start_ts = self.current_timestamp + return + + def should_buy_spot_short_perp(self) -> bool: + spot_buy_price = self.limit_taker_price(self.spot_connector, is_buy=True) + perp_sell_price = self.limit_taker_price(self.perp_connector, is_buy=False) + ret_pbs = float((perp_sell_price - spot_buy_price) / spot_buy_price) * 10000 + is_profitable = ret_pbs >= self.buy_spot_short_perp_profit_margin_bps + is_repeat = self.last_strategy_action == StrategyAction.BUY_SPOT_SHORT_PERP + return is_profitable and not is_repeat + + # TODO: check if balance is deducted when it has position + def can_buy_spot_short_perp(self) -> bool: + spot_balance = self.get_balance(self.spot_connector, is_base=False) + buy_price_with_slippage = self.limit_taker_price_with_slippage( + self.spot_connector, is_buy=True + ) + spot_required = buy_price_with_slippage * self.base_order_amount + is_spot_enough = Decimal(spot_balance) >= spot_required + if not is_spot_enough: + _, quote = split_hb_trading_pair(self.trading_pair) + float_spot_required = float(spot_required) + self.logger().info( + f"Insufficient balance in {self.spot_connector}: {spot_balance} {quote}. " + f"Required {float_spot_required:.4f} {quote}." + ) + perp_balance = self.get_balance(self.perp_connector, is_base=False) + # short order WITHOUT any splippage takes more capital + short_price = self.limit_taker_price(self.perp_connector, is_buy=False) + perp_required = short_price * self.base_order_amount + is_perp_enough = Decimal(perp_balance) >= perp_required + if not is_perp_enough: + _, quote = split_hb_trading_pair(self.trading_pair) + float_perp_required = float(perp_required) + self.logger().info( + f"Insufficient balance in {self.perp_connector}: {perp_balance:.4f} {quote}. " + f"Required {float_perp_required:.4f} {quote}." + ) + return is_spot_enough and is_perp_enough + + # TODO: use OrderCandidate and check for budget + def buy_spot_short_perp(self) -> None: + spot_buy_price_with_slippage = self.limit_taker_price_with_slippage( + self.spot_connector, is_buy=True + ) + perp_short_price_with_slippage = self.limit_taker_price_with_slippage( + self.perp_connector, is_buy=False + ) + spot_buy_price = self.limit_taker_price(self.spot_connector, is_buy=True) + perp_short_price = self.limit_taker_price(self.perp_connector, is_buy=False) + + self.buy( + self.spot_connector, + self.trading_pair, + amount=self.base_order_amount, + order_type=OrderType.LIMIT, + price=spot_buy_price_with_slippage, + ) + trade_state_log = self.trade_state_log() + + self.logger().info( + f"Submitted buy order in {self.spot_connector} for {self.trading_pair} " + f"at price {spot_buy_price_with_slippage:.06f}@{self.base_order_amount} to {trade_state_log}. (Buy price without slippage: {spot_buy_price})" + ) + position_action = self.perp_trade_position_action() + self.sell( + self.perp_connector, + self.trading_pair, + amount=self.base_order_amount, + order_type=OrderType.LIMIT, + price=perp_short_price_with_slippage, + position_action=position_action, + ) + self.logger().info( + f"Submitted short order in {self.perp_connector} for {self.trading_pair} " + f"at price {perp_short_price_with_slippage:.06f}@{self.base_order_amount} to {trade_state_log}. (Short price without slippage: {perp_short_price})" + ) + + self.opened_state_start_ts = self.current_timestamp + return + + def should_sell_spot_long_perp(self) -> bool: + spot_sell_price = self.limit_taker_price(self.spot_connector, is_buy=False) + perp_buy_price = self.limit_taker_price(self.perp_connector, is_buy=True) + ret_pbs = float((spot_sell_price - perp_buy_price) / perp_buy_price) * 10000 + is_profitable = ret_pbs >= self.sell_spot_long_perp_profit_margin_bps + is_repeat = self.last_strategy_action == StrategyAction.SELL_SPOT_LONG_PERP + return is_profitable and not is_repeat + + def can_sell_spot_long_perp(self) -> bool: + spot_balance = self.get_balance(self.spot_connector, is_base=True) + spot_required = self.base_order_amount + is_spot_enough = Decimal(spot_balance) >= spot_required + if not is_spot_enough: + base, _ = split_hb_trading_pair(self.trading_pair) + float_spot_required = float(spot_required) + self.logger().info( + f"Insufficient balance in {self.spot_connector}: {spot_balance} {base}. " + f"Required {float_spot_required:.4f} {base}." + ) + perp_balance = self.get_balance(self.perp_connector, is_base=False) + # long order WITH any splippage takes more capital + long_price_with_slippage = self.limit_taker_price( + self.perp_connector, is_buy=True + ) + perp_required = long_price_with_slippage * self.base_order_amount + is_perp_enough = Decimal(perp_balance) >= perp_required + if not is_perp_enough: + _, quote = split_hb_trading_pair(self.trading_pair) + float_perp_required = float(perp_required) + self.logger().info( + f"Insufficient balance in {self.perp_connector}: {perp_balance:.4f} {quote}. " + f"Required {float_perp_required:.4f} {quote}." + ) + return is_spot_enough and is_perp_enough + + def sell_spot_long_perp(self) -> None: + perp_long_price_with_slippage = self.limit_taker_price_with_slippage( + self.perp_connector, is_buy=True + ) + spot_sell_price_with_slippage = self.limit_taker_price_with_slippage( + self.spot_connector, is_buy=False + ) + perp_long_price = self.limit_taker_price(self.perp_connector, is_buy=True) + spot_sell_price = self.limit_taker_price(self.spot_connector, is_buy=False) + + position_action = self.perp_trade_position_action() + self.buy( + self.perp_connector, + self.trading_pair, + amount=self.base_order_amount, + order_type=OrderType.LIMIT, + price=perp_long_price_with_slippage, + position_action=position_action, + ) + trade_state_log = self.trade_state_log() + self.logger().info( + f"Submitted long order in {self.perp_connector} for {self.trading_pair} " + f"at price {perp_long_price_with_slippage:.06f}@{self.base_order_amount} to {trade_state_log}. (Long price without slippage: {perp_long_price})" + ) + self.sell( + self.spot_connector, + self.trading_pair, + amount=self.base_order_amount, + order_type=OrderType.LIMIT, + price=spot_sell_price_with_slippage, + ) + self.logger().info( + f"Submitted sell order in {self.spot_connector} for {self.trading_pair} " + f"at price {spot_sell_price_with_slippage:.06f}@{self.base_order_amount} to {trade_state_log}. (Sell price without slippage: {spot_sell_price})" + ) + + self.opened_state_start_ts = self.current_timestamp + return + + def limit_taker_price_with_slippage( + self, connector_name: str, is_buy: bool + ) -> Decimal: + price = self.limit_taker_price(connector_name, is_buy) + slippage = ( + Decimal(1 + self.slippage_buffer_bps / 10000) + if is_buy + else Decimal(1 - self.slippage_buffer_bps / 10000) + ) + return price * slippage + + def limit_taker_price(self, connector_name: str, is_buy: bool) -> Decimal: + limit_taker_price_result = self.connectors[connector_name].get_price_for_volume( + self.trading_pair, is_buy, self.base_order_amount + ) + return limit_taker_price_result.result_price + + def get_balance(self, connector_name: str, is_base: bool) -> float: + if connector_name == self.perp_connector: + assert not is_base, "Perpetual connector does not have base asset" + base, quote = split_hb_trading_pair(self.trading_pair) + balance = self.connectors[connector_name].get_available_balance( + base if is_base else quote + ) + return float(balance) + + def trade_state_log(self) -> str: + if self.strategy_state == StrategyState.Opening: + return "open position" + elif self.strategy_state == StrategyState.Closing: + return "close position" + else: + raise ValueError( + f"Strategy state: {self.strategy_state} shouldnt happen during trade." + ) + + def perp_trade_position_action(self) -> PositionAction: + if self.strategy_state == StrategyState.Opening: + return PositionAction.OPEN + elif self.strategy_state == StrategyState.Closing: + return PositionAction.CLOSE + else: + raise ValueError( + f"Strategy state: {self.strategy_state} shouldnt happen during trade." + ) + + def format_status(self) -> str: + if not self.ready_to_trade: + return "Market connectors are not ready." + + lines: List[str] = [] + self._append_buy_spot_short_perp_status(lines) + lines.extend(["", ""]) + self._append_sell_spot_long_perp_status(lines) + lines.extend(["", ""]) + self._append_balances_status(lines) + lines.extend(["", ""]) + self._append_bot_states(lines) + lines.extend(["", ""]) + return "\n".join(lines) + + def _append_buy_spot_short_perp_status(self, lines: List[str]) -> None: + spot_buy_price = self.limit_taker_price(self.spot_connector, is_buy=True) + perp_short_price = self.limit_taker_price(self.perp_connector, is_buy=False) + return_pbs = ( + float((perp_short_price - spot_buy_price) / spot_buy_price) * 100 * 100 + ) + lines.append(f"Buy Spot Short Perp Opportunity ({self.trading_pair}):") + lines.append(f"Buy Spot: {spot_buy_price}") + lines.append(f"Short Perp: {perp_short_price}") + lines.append(f"Return (bps): {return_pbs:.1f}%") + return + + def _append_sell_spot_long_perp_status(self, lines: List[str]) -> None: + perp_long_price = self.limit_taker_price(self.perp_connector, is_buy=True) + spot_sell_price = self.limit_taker_price(self.spot_connector, is_buy=False) + return_pbs = ( + float((spot_sell_price - perp_long_price) / perp_long_price) * 100 * 100 + ) + lines.append(f"Long Perp Sell Spot Opportunity ({self.trading_pair}):") + lines.append(f"Long Perp: {perp_long_price}") + lines.append(f"Sell Spot: {spot_sell_price}") + lines.append(f"Return (bps): {return_pbs:.1f}%") + return + + def _append_balances_status(self, lines: List[str]) -> None: + base, quote = split_hb_trading_pair(self.trading_pair) + spot_base_balance = self.get_balance(self.spot_connector, is_base=True) + spot_quote_balance = self.get_balance(self.spot_connector, is_base=False) + perp_quote_balance = self.get_balance(self.perp_connector, is_base=False) + lines.append("Balances:") + lines.append(f"Spot Base Balance: {spot_base_balance:.04f} {base}") + lines.append(f"Spot Quote Balance: {spot_quote_balance:.04f} {quote}") + lines.append(f"Perp Balance: {perp_quote_balance:04f} USDT") + return + + def _append_bot_states(self, lines: List[str]) -> None: + lines.append("Bot States:") + lines.append(f"Current Timestamp: {self.current_timestamp}") + lines.append(f"Strategy State: {self.strategy_state.name}") + lines.append(f"Open Next Opportunity after: {self.next_arbitrage_opening_ts}") + lines.append(f"Last In Flight State at: {self.in_flight_state_start_ts}") + lines.append(f"Last Opened State at: {self.opened_state_start_ts}") + lines.append(f"Completed Ordered IDs: {self.completed_order_ids}") + return + + def did_complete_buy_order(self, event: BuyOrderCompletedEvent) -> None: + self.completed_order_ids.append(event.order_id) + + def did_complete_sell_order(self, event: SellOrderCompletedEvent) -> None: + self.completed_order_ids.append(event.order_id) + + def did_change_position_mode_succeed(self, _): + self.logger().info( + f"Completed setting position mode to ONEWAY for {self.perp_connector}" + ) + self.is_position_mode_ready = True + + def did_change_position_mode_fail( + self, position_mode_changed_event: PositionModeChangeEvent + ): + self.logger().error( + "Failed to set position mode to ONEWAY. " + f"Reason: {position_mode_changed_event.message}." + ) + self.logger().warning( + "Cannot continue. Please resolve the issue in the account." + ) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/stat_arb.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/stat_arb.py new file mode 100644 index 0000000..faf5c52 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/stat_arb.py @@ -0,0 +1,199 @@ +from decimal import Decimal + +import pandas as pd +import pandas_ta as ta + +from hummingbot.core.data_type.common import TradeType, PriceType +from hummingbot.data_feed.candles_feed.candles_factory import CandlesFactory +from hummingbot.smart_components.position_executor.data_types import PositionConfig +from hummingbot.smart_components.position_executor.position_executor import PositionExecutor +from hummingbot.strategy.directional_strategy_base import DirectionalStrategyBase + + +class StatisticalArbitrageLeft(DirectionalStrategyBase): + """ + BotCamp Cohort #5 July 2023 + Design Template: https://github.com/hummingbot/hummingbot-botcamp/issues/48 + + Description: + Statistical Arbitrage strategy implementation based on the DirectionalStrategyBase. + This strategy execute trades based on the Z-score values. + This strategy is divided into a left and right side code. + Left side code is statistical_arbitrage_left.py. + Right side code is statistical_arbitrage_right.py. + This code the left side of this strategy + When z-score indicates an entry signal. the left side will execute a long position and right side will execute a short position. + When z-score indicates an exit signal. the left side will execute a short position and right side will execute a long position. + """ + directional_strategy_name: str = "statistical_arbitrage" + # Define the trading pair and exchange that we want to use and the csv where we are going to store the entries + trading_pair: str = "ETH-USDT" # left side trading pair + trading_pair_2: str = "MATIC-USDT" # right side trading pair + exchange: str = "binance_perpetual" + order_amount_usd = Decimal("65") + leverage = 10 + length = 100 + max_executors = 2 + max_hours_to_hold_position = 12 + + # Configure the parameters for the position + zscore_long_threshold: int = -1.5 + zscore_short_threshold: int = 1.5 + + arbitrage_take_profit = Decimal("0.01") + arbitrage_stop_loss = Decimal("0.02") + + candles = [ + CandlesFactory.get_candle(connector=exchange, + trading_pair=trading_pair, + interval="1h", max_records=1000), + CandlesFactory.get_candle(connector=exchange, + trading_pair=trading_pair_2, + interval="1h", max_records=1000), + ] + last_signal = 0 + report_frequency_in_hours = 6 + next_report_time = 0 + markets = {exchange: {trading_pair, trading_pair_2}} + + def on_tick(self): + self.check_and_send_report() + self.clean_and_store_executors() + if self.is_perpetual: + self.check_and_set_leverage() + + if self.all_candles_ready: + signal = self.get_signal() + if len(self.active_executors) == 0: + position_configs = self.get_arbitrage_position_configs(signal) + if position_configs: + self.last_signal = signal + for position_config in position_configs: + executor = PositionExecutor(strategy=self, + position_config=position_config) + self.active_executors.append(executor) + else: + consolidated_pnl = self.get_unrealized_pnl() + if consolidated_pnl > self.arbitrage_take_profit or consolidated_pnl < -self.arbitrage_stop_loss: + self.logger().info("Exit Arbitrage") + for executor in self.active_executors: + executor.early_stop() + self.last_signal = 0 + + def get_arbitrage_position_configs(self, signal): + trading_pair_1_amount, trading_pair_2_amount = self.get_order_amounts() + if signal == 1: + buy_config = PositionConfig( + timestamp=self.current_timestamp, + trading_pair=self.trading_pair, + exchange=self.exchange, + side=TradeType.BUY, + amount=trading_pair_1_amount, + leverage=self.leverage, + time_limit=int(60 * 60 * self.max_hours_to_hold_position), + ) + sell_config = PositionConfig( + timestamp=self.current_timestamp, + trading_pair=self.trading_pair_2, + exchange=self.exchange, + side=TradeType.SELL, + amount=trading_pair_2_amount, + leverage=self.leverage, + time_limit=int(60 * 60 * self.max_hours_to_hold_position), + ) + return [buy_config, sell_config] + elif signal == -1: + buy_config = PositionConfig( + timestamp=self.current_timestamp, + trading_pair=self.trading_pair_2, + exchange=self.exchange, + side=TradeType.BUY, + amount=trading_pair_2_amount, + leverage=self.leverage, + time_limit=int(60 * 60 * self.max_hours_to_hold_position), + ) + sell_config = PositionConfig( + timestamp=self.current_timestamp, + trading_pair=self.trading_pair, + exchange=self.exchange, + side=TradeType.SELL, + amount=trading_pair_1_amount, + leverage=self.leverage, + time_limit=int(60 * 60 * self.max_hours_to_hold_position), + ) + return [buy_config, sell_config] + + def get_order_amounts(self): + base_quantized_1, usd_quantized_1 = self.get_order_amount_quantized_in_base_and_usd(self.trading_pair, self.order_amount_usd) + base_quantized_2, usd_quantized_2 = self.get_order_amount_quantized_in_base_and_usd(self.trading_pair_2, self.order_amount_usd) + if usd_quantized_2 > usd_quantized_1: + base_quantized_2, usd_quantized_2 = self.get_order_amount_quantized_in_base_and_usd(self.trading_pair_2, usd_quantized_1) + elif usd_quantized_1 > usd_quantized_2: + base_quantized_1, usd_quantized_1 = self.get_order_amount_quantized_in_base_and_usd(self.trading_pair, usd_quantized_2) + return base_quantized_1, base_quantized_2 + + def get_order_amount_quantized_in_base_and_usd(self, trading_pair: str, order_amount_usd: Decimal): + price = self.connectors[self.exchange].get_price_by_type(trading_pair, PriceType.MidPrice) + amount_quantized = self.connectors[self.exchange].quantize_order_amount(trading_pair, order_amount_usd / price) + return amount_quantized, amount_quantized * price + + def get_signal(self): + candles_df = self.get_processed_df() + z_score = candles_df.iat[-1, -1] + # all execution are only on the left side trading pair + if z_score < self.zscore_long_threshold: + return 1 + elif z_score > self.zscore_short_threshold: + return -1 + else: + return 0 + + def get_processed_df(self): + candles_df_1 = self.candles[0].candles_df + candles_df_2 = self.candles[1].candles_df + + # calculate the spread and z-score based on the candles of 2 trading pairs + df = pd.merge(candles_df_1, candles_df_2, on="timestamp", how='inner', suffixes=('', '_2')) + hedge_ratio = df["close"].tail(self.length).mean() / df["close_2"].tail(self.length).mean() + + df["spread"] = df["close"] - (df["close_2"] * hedge_ratio) + df["z_score"] = ta.zscore(df["spread"], length=self.length) + return df + + def market_data_extra_info(self): + """ + Provides additional information about the market data to the format status. + Returns: + List[str]: A list of formatted strings containing market data information. + """ + lines = [] + columns_to_show = ["timestamp", "open", "low", "high", "close", "volume", "z_score", "close_2"] + candles_df = self.get_processed_df() + distance_to_target = self.get_unrealized_pnl() - self.arbitrage_take_profit + lines.extend( + [f"Consolidated PNL (%): {self.get_unrealized_pnl() * 100:.2f} | Target (%): {self.arbitrage_take_profit * 100:.2f} | Diff: {distance_to_target * 100:.2f}"], + ) + lines.extend([f"Candles: {self.candles[0].name} | Interval: {self.candles[0].interval}\n"]) + lines.extend(self.candles_formatted_list(candles_df, columns_to_show)) + return lines + + def get_unrealized_pnl(self): + cum_pnl = 0 + for executor in self.active_executors: + cum_pnl += executor.net_pnl + return cum_pnl + + def get_realized_pnl(self): + cum_pnl = 0 + for executor in self.stored_executors: + cum_pnl += executor.net_pnl + return cum_pnl + + def check_and_send_report(self): + if self.current_timestamp > self.next_report_time: + self.notify_hb_app_with_timestamp(f""" +Closed Positions: {len(self.stored_executors)} | Realized PNL (%): {self.get_realized_pnl() * 100:.2f} +Open Positions: {len(self.active_executors)} | Unrealized PNL (%): {self.get_unrealized_pnl() * 100:.2f} +""" + ) + self.next_report_time = self.current_timestamp + 60 * 60 * self.report_frequency_in_hours diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/triangular_arbitrage.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/triangular_arbitrage.py new file mode 100644 index 0000000..6f6186b --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/triangular_arbitrage.py @@ -0,0 +1,456 @@ +import logging +import math + +from hummingbot.connector.utils import split_hb_trading_pair +from hummingbot.core.data_type.common import TradeType +from hummingbot.core.data_type.order_candidate import OrderCandidate +from hummingbot.core.event.events import ( + BuyOrderCompletedEvent, + BuyOrderCreatedEvent, + MarketOrderFailureEvent, + SellOrderCompletedEvent, + SellOrderCreatedEvent, +) +from hummingbot.strategy.script_strategy_base import Decimal, OrderType, ScriptStrategyBase + + +class TriangularArbitrage(ScriptStrategyBase): + """ + BotCamp Cohort: Sept 2022 + Design Template: https://hummingbot-foundation.notion.site/Triangular-Arbitrage-07ef29ee97d749e1afa798a024813c88 + Video: https://www.loom.com/share/b6781130251945d4b51d6de3f8434047 + Description: + This script executes arbitrage trades on 3 markets of the same exchange when a price discrepancy + among those markets found. + + - All orders are executed linearly. That is the second order is placed after the first one is + completely filled and the third order is placed after the second. + - The script allows you to hold mainly one asset in your inventory (holding_asset). + - It always starts trades round by selling the holding asset and ends by buying it. + - There are 2 possible arbitrage trades directions: "direct" and "reverse". + Example with USDT holding asset: + 1. Direct: buy ADA-USDT > sell ADA-BTC > sell BTC-USDT + 2. Reverse: buy BTC-USDT > buy ADA-BTC > sell ADA-USDT + - The order amount is fixed and set in holding asset + - The strategy has 2nd and 3d orders creation check and makes several trials if there is a failure + - Profit is calculated each round and total profit is checked for the kill_switch to prevent from excessive losses + - !!! Profitability calculation doesn't take into account trading fees, set min_profitability to at least 3 * fee + """ + # Config params + connector_name: str = "kucoin" + first_pair: str = "ADA-USDT" + second_pair: str = "ADA-BTC" + third_pair: str = "BTC-USDT" + holding_asset: str = "USDT" + + min_profitability: Decimal = Decimal("0.5") + order_amount_in_holding_asset: Decimal = Decimal("20") + + kill_switch_enabled: bool = True + kill_switch_rate = Decimal("-2") + + # Class params + status: str = "NOT_INIT" + trading_pair: dict = {} + order_side: dict = {} + profit: dict = {} + order_amount: dict = {} + profitable_direction: str = "" + place_order_trials_count: int = 0 + place_order_trials_limit: int = 10 + place_order_failure: bool = False + order_candidate = None + initial_spent_amount = Decimal("0") + total_profit = Decimal("0") + total_profit_pct = Decimal("0") + + markets = {connector_name: {first_pair, second_pair, third_pair}} + + @property + def connector(self): + """ + The only connector in this strategy, define it here for easy access + """ + return self.connectors[self.connector_name] + + def on_tick(self): + """ + Every tick the strategy calculates the profitability of both direct and reverse direction. + If the profitability of any direction is large enough it starts the arbitrage by creating and processing + the first order candidate. + """ + if self.status == "NOT_INIT": + self.init_strategy() + + if self.arbitrage_started(): + return + + if not self.ready_for_new_orders(): + return + + self.profit["direct"], self.order_amount["direct"] = self.calculate_profit(self.trading_pair["direct"], + self.order_side["direct"]) + self.profit["reverse"], self.order_amount["reverse"] = self.calculate_profit(self.trading_pair["reverse"], + self.order_side["reverse"]) + self.log_with_clock(logging.INFO, f"Profit direct: {round(self.profit['direct'], 2)}, " + f"Profit reverse: {round(self.profit['reverse'], 2)}") + + if self.profit["direct"] < self.min_profitability and self.profit["reverse"] < self.min_profitability: + return + + self.profitable_direction = "direct" if self.profit["direct"] > self.profit["reverse"] else "reverse" + self.start_arbitrage(self.trading_pair[self.profitable_direction], + self.order_side[self.profitable_direction], + self.order_amount[self.profitable_direction]) + + def init_strategy(self): + """ + Initializes strategy once before the start. + """ + self.status = "ACTIVE" + self.check_trading_pair() + self.set_trading_pair() + self.set_order_side() + + def check_trading_pair(self): + """ + Checks if the pairs specified in the config are suitable for the triangular arbitrage. + They should have only 3 common assets with holding_asset among them. + """ + base_1, quote_1 = split_hb_trading_pair(self.first_pair) + base_2, quote_2 = split_hb_trading_pair(self.second_pair) + base_3, quote_3 = split_hb_trading_pair(self.third_pair) + all_assets = {base_1, base_2, base_3, quote_1, quote_2, quote_3} + if len(all_assets) != 3 or self.holding_asset not in all_assets: + self.status = "NOT_ACTIVE" + self.log_with_clock(logging.WARNING, f"Pairs {self.first_pair}, {self.second_pair}, {self.third_pair} " + f"are not suited for triangular arbitrage!") + + def set_trading_pair(self): + """ + Rearrange trading pairs so that the first and last pair contains holding asset. + We start trading round by selling holding asset and finish by buying it. + Makes 2 tuples for "direct" and "reverse" directions and assigns them to the corresponding dictionary. + """ + if self.holding_asset not in self.first_pair: + pairs_ordered = (self.second_pair, self.first_pair, self.third_pair) + elif self.holding_asset not in self.second_pair: + pairs_ordered = (self.first_pair, self.second_pair, self.third_pair) + else: + pairs_ordered = (self.first_pair, self.third_pair, self.second_pair) + + self.trading_pair["direct"] = pairs_ordered + self.trading_pair["reverse"] = pairs_ordered[::-1] + + def set_order_side(self): + """ + Sets order sides (1 = buy, 0 = sell) for already ordered trading pairs. + Makes 2 tuples for "direct" and "reverse" directions and assigns them to the corresponding dictionary. + """ + base_1, quote_1 = split_hb_trading_pair(self.trading_pair["direct"][0]) + base_2, quote_2 = split_hb_trading_pair(self.trading_pair["direct"][1]) + base_3, quote_3 = split_hb_trading_pair(self.trading_pair["direct"][2]) + + order_side_1 = 0 if base_1 == self.holding_asset else 1 + order_side_2 = 0 if base_1 == base_2 else 1 + order_side_3 = 1 if base_3 == self.holding_asset else 0 + + self.order_side["direct"] = (order_side_1, order_side_2, order_side_3) + self.order_side["reverse"] = (1 - order_side_3, 1 - order_side_2, 1 - order_side_1) + + def arbitrage_started(self) -> bool: + """ + Checks for an unfinished arbitrage round. + If there is a failure in placing 2nd or 3d order tries to place an order again + until place_order_trials_limit reached. + """ + if self.status == "ARBITRAGE_STARTED": + if self.order_candidate and self.place_order_failure: + if self.place_order_trials_count <= self.place_order_trials_limit: + self.log_with_clock(logging.INFO, f"Failed to place {self.order_candidate.trading_pair} " + f"{self.order_candidate.order_side} order. Trying again!") + self.process_candidate(self.order_candidate, True) + else: + msg = f"Error placing {self.order_candidate.trading_pair} {self.order_candidate.order_side} order" + self.notify_hb_app_with_timestamp(msg) + self.log_with_clock(logging.WARNING, msg) + self.status = "NOT_ACTIVE" + return True + + return False + + def ready_for_new_orders(self) -> bool: + """ + Checks if we are ready for new orders: + - Current status check + - Holding asset balance check + Return boolean True if we are ready and False otherwise + """ + if self.status == "NOT_ACTIVE": + return False + + if self.connector.get_available_balance(self.holding_asset) < self.order_amount_in_holding_asset: + self.log_with_clock(logging.INFO, + f"{self.connector_name} {self.holding_asset} balance is too low. Cannot place order.") + return False + + return True + + def calculate_profit(self, trading_pair, order_side): + """ + Calculates profitability and order amounts for 3 trading pairs based on the orderbook depth. + """ + exchanged_amount = self.order_amount_in_holding_asset + order_amount = [0, 0, 0] + + for i in range(3): + order_amount[i] = self.get_order_amount_from_exchanged_amount(trading_pair[i], order_side[i], + exchanged_amount) + # Update exchanged_amount for the next cycle + if order_side[i]: + exchanged_amount = order_amount[i] + else: + exchanged_amount = self.connector.get_quote_volume_for_base_amount(trading_pair[i], order_side[i], + order_amount[i]).result_volume + start_amount = self.order_amount_in_holding_asset + end_amount = exchanged_amount + profit = (end_amount / start_amount - 1) * 100 + + return profit, order_amount + + def get_order_amount_from_exchanged_amount(self, pair, side, exchanged_amount) -> Decimal: + """ + Calculates order amount using the amount that we want to exchange. + - If the side is buy then exchanged asset is a quote asset. Get base amount using the orderbook + - If the side is sell then exchanged asset is a base asset. + """ + if side: + orderbook = self.connector.get_order_book(pair) + order_amount = self.get_base_amount_for_quote_volume(orderbook.ask_entries(), exchanged_amount) + else: + order_amount = exchanged_amount + + return order_amount + + def get_base_amount_for_quote_volume(self, orderbook_entries, quote_volume) -> Decimal: + """ + Calculates base amount that you get for the quote volume using the orderbook entries + """ + cumulative_volume = 0. + cumulative_base_amount = 0. + quote_volume = float(quote_volume) + + for order_book_row in orderbook_entries: + row_amount = order_book_row.amount + row_price = order_book_row.price + row_volume = row_amount * row_price + if row_volume + cumulative_volume >= quote_volume: + row_volume = quote_volume - cumulative_volume + row_amount = row_volume / row_price + cumulative_volume += row_volume + cumulative_base_amount += row_amount + if cumulative_volume >= quote_volume: + break + + return Decimal(cumulative_base_amount) + + def start_arbitrage(self, trading_pair, order_side, order_amount): + """ + Starts arbitrage by creating and processing the first order candidate + """ + first_candidate = self.create_order_candidate(trading_pair[0], order_side[0], order_amount[0]) + if first_candidate: + if self.process_candidate(first_candidate, False): + self.status = "ARBITRAGE_STARTED" + + def create_order_candidate(self, pair, side, amount): + """ + Creates order candidate. Checks the quantized amount + """ + side = TradeType.BUY if side else TradeType.SELL + price = self.connector.get_price_for_volume(pair, side, amount).result_price + price_quantize = self.connector.quantize_order_price(pair, Decimal(price)) + amount_quantize = self.connector.quantize_order_amount(pair, Decimal(amount)) + + if amount_quantize == Decimal("0"): + self.log_with_clock(logging.INFO, f"Order amount on {pair} is too low to place an order") + return None + + return OrderCandidate( + trading_pair=pair, + is_maker=False, + order_type=OrderType.MARKET, + order_side=side, + amount=amount_quantize, + price=price_quantize) + + def process_candidate(self, order_candidate, multiple_trials_enabled) -> bool: + """ + Checks order candidate balance and either places an order or sets a failure for the next trials + """ + order_candidate_adjusted = self.connector.budget_checker.adjust_candidate(order_candidate, all_or_none=True) + if math.isclose(order_candidate.amount, Decimal("0"), rel_tol=1E-6): + self.logger().info(f"Order adjusted amount: {order_candidate.amount} on {order_candidate.trading_pair}, " + f"too low to place an order") + if multiple_trials_enabled: + self.place_order_trials_count += 1 + self.place_order_failure = True + return False + else: + is_buy = True if order_candidate.order_side == TradeType.BUY else False + self.place_order(self.connector_name, + order_candidate.trading_pair, + is_buy, + order_candidate_adjusted.amount, + order_candidate.order_type, + order_candidate_adjusted.price) + return True + + def place_order(self, + connector_name: str, + trading_pair: str, + is_buy: bool, + amount: Decimal, + order_type: OrderType, + price=Decimal("NaN"), + ): + if is_buy: + self.buy(connector_name, trading_pair, amount, order_type, price) + else: + self.sell(connector_name, trading_pair, amount, order_type, price) + + # Events + def did_create_buy_order(self, event: BuyOrderCreatedEvent): + self.log_with_clock(logging.INFO, f"Buy order is created on the market {event.trading_pair}") + if self.order_candidate: + if self.order_candidate.trading_pair == event.trading_pair: + self.reset_order_candidate() + + def did_create_sell_order(self, event: SellOrderCreatedEvent): + self.log_with_clock(logging.INFO, f"Sell order is created on the market {event.trading_pair}") + if self.order_candidate: + if self.order_candidate.trading_pair == event.trading_pair: + self.reset_order_candidate() + + def reset_order_candidate(self): + """ + Deletes order candidate variable and resets counter + """ + self.order_candidate = None + self.place_order_trials_count = 0 + self.place_order_failure = False + + def did_fail_order(self, event: MarketOrderFailureEvent): + if self.order_candidate: + self.place_order_failure = True + + def did_complete_buy_order(self, event: BuyOrderCompletedEvent): + msg = f"Buy {round(event.base_asset_amount, 6)} {event.base_asset} " \ + f"for {round(event.quote_asset_amount, 6)} {event.quote_asset} is completed" + self.notify_hb_app_with_timestamp(msg) + self.log_with_clock(logging.INFO, msg) + self.process_next_pair(event) + + def did_complete_sell_order(self, event: SellOrderCompletedEvent): + msg = f"Sell {round(event.base_asset_amount, 6)} {event.base_asset} " \ + f"for {round(event.quote_asset_amount, 6)} {event.quote_asset} is completed" + self.notify_hb_app_with_timestamp(msg) + self.log_with_clock(logging.INFO, msg) + self.process_next_pair(event) + + def process_next_pair(self, order_event): + """ + Processes 2nd or 3d order and finalizes the arbitrage + - Gets the completed order index + - Calculates order amount + - Creates and processes order candidate + - Finalizes arbitrage if the 3d order was completed + """ + event_pair = f"{order_event.base_asset}-{order_event.quote_asset}" + trading_pair = self.trading_pair[self.profitable_direction] + order_side = self.order_side[self.profitable_direction] + + event_order_index = trading_pair.index(event_pair) + + if order_side[event_order_index]: + exchanged_amount = order_event.base_asset_amount + else: + exchanged_amount = order_event.quote_asset_amount + + # Save initial amount spent for further profit calculation + if event_order_index == 0: + self.initial_spent_amount = order_event.quote_asset_amount if order_side[event_order_index] \ + else order_event.base_asset_amount + + if event_order_index < 2: + order_amount = self.get_order_amount_from_exchanged_amount(trading_pair[event_order_index + 1], + order_side[event_order_index + 1], + exchanged_amount) + self.order_candidate = self.create_order_candidate(trading_pair[event_order_index + 1], + order_side[event_order_index + 1], order_amount) + if self.order_candidate: + self.process_candidate(self.order_candidate, True) + else: + self.finalize_arbitrage(exchanged_amount) + + def finalize_arbitrage(self, final_exchanged_amount): + """ + Finalizes arbitrage + - Calculates trading round profit + - Updates total profit + - Checks the kill switch threshold + """ + order_profit = round(final_exchanged_amount - self.initial_spent_amount, 6) + order_profit_pct = round(100 * order_profit / self.initial_spent_amount, 2) + msg = f"*** Arbitrage completed! Profit: {order_profit} {self.holding_asset} ({order_profit_pct})%" + self.log_with_clock(logging.INFO, msg) + self.notify_hb_app_with_timestamp(msg) + + self.total_profit += order_profit + self.total_profit_pct = round(100 * self.total_profit / self.order_amount_in_holding_asset, 2) + self.status = "ACTIVE" + if self.kill_switch_enabled and self.total_profit_pct < self.kill_switch_rate: + self.status = "NOT_ACTIVE" + self.log_with_clock(logging.INFO, "Kill switch threshold reached. Stop trading") + self.notify_hb_app_with_timestamp("Kill switch threshold reached. Stop trading") + + def format_status(self) -> str: + """ + Returns status of the current strategy, total profit, current profitability of possible trades and balances. + This function is called when status command is issued. + """ + if not self.ready_to_trade: + return "Market connectors are not ready." + lines = [] + warning_lines = [] + warning_lines.extend(self.network_warning(self.get_market_trading_pair_tuples())) + + lines.extend(["", " Strategy status:"] + [" " + self.status]) + + lines.extend(["", " Total profit:"] + [" " + f"{self.total_profit} {self.holding_asset}" + f"({self.total_profit_pct}%)"]) + + for direction in self.trading_pair: + pairs_str = [f"{'buy' if side else 'sell'} {pair}" + for side, pair in zip(self.order_side[direction], self.trading_pair[direction])] + pairs_str = " > ".join(pairs_str) + profit_str = str(round(self.profit[direction], 2)) + lines.extend(["", f" {direction.capitalize()}:", f" {pairs_str}", f" profitability: {profit_str}%"]) + + balance_df = self.get_balance_df() + lines.extend(["", " Balances:"] + [" " + line for line in balance_df.to_string(index=False).split("\n")]) + + try: + df = self.active_orders_df() + lines.extend(["", " Orders:"] + [" " + line for line in df.to_string(index=False).split("\n")]) + except ValueError: + lines.extend(["", " No active orders."]) + + if self.connector.get_available_balance(self.holding_asset) < self.order_amount_in_holding_asset: + warning_lines.extend( + [f"{self.connector_name} {self.holding_asset} balance is too low. Cannot place order."]) + + if len(warning_lines) > 0: + lines.extend(["", "*** WARNINGS ***"] + warning_lines) + + return "\n".join(lines) diff --git a/hummingbot_files/bot_configs/master_bot_conf/scripts/wallet_hedge_example.py b/hummingbot_files/bot_configs/master_bot_conf/scripts/wallet_hedge_example.py new file mode 100644 index 0000000..6d74d67 --- /dev/null +++ b/hummingbot_files/bot_configs/master_bot_conf/scripts/wallet_hedge_example.py @@ -0,0 +1,90 @@ +from decimal import Decimal +from typing import Dict + +from hummingbot.client.ui.interface_utils import format_df_for_printout +from hummingbot.connector.connector_base import ConnectorBase +from hummingbot.core.data_type.common import OrderType +from hummingbot.data_feed.wallet_tracker_data_feed import WalletTrackerDataFeed +from hummingbot.strategy.script_strategy_base import ScriptStrategyBase + + +class WalletHedgeExample(ScriptStrategyBase): + # Wallet params + token = "WETH" + wallet_balance_data_feed = WalletTrackerDataFeed( + chain="ethereum", + network="goerli", + wallets={"0xDA50C69342216b538Daf06FfECDa7363E0B96684"}, + tokens={token}, + ) + hedge_threshold = 0.05 + + # Hedge params + hedge_exchange = "kucoin_paper_trade" + hedge_pair = "ETH-USDT" + base, quote = hedge_pair.split("-") + + # Balances variables + balance = 0 + balance_start = 0 + balance_delta = 0 + balance_hedge = 0 + exchange_balance_start = 0 + exchange_balance = 0 + + markets = {hedge_exchange: {hedge_pair}} + + def __init__(self, connectors: Dict[str, ConnectorBase]): + super().__init__(connectors) + self.wallet_balance_data_feed.start() + + def on_stop(self): + self.wallet_balance_data_feed.stop() + + def on_tick(self): + self.balance = self.wallet_balance_data_feed.wallet_balances_df[self.token].sum() + self.exchange_balance = self.get_exchange_base_asset_balance() + + if self.balance_start == 0: # first run + self.balance_start = self.balance + self.balance_hedge = self.balance + self.exchange_balance_start = self.get_exchange_base_asset_balance() + else: + self.balance_delta = self.balance - self.balance_hedge + + mid_price = self.connectors[self.hedge_exchange].get_mid_price(self.hedge_pair) + if self.balance_delta > 0 and self.balance_delta >= self.hedge_threshold: + self.sell(self.hedge_exchange, self.hedge_pair, self.balance_delta, OrderType.MARKET, mid_price) + self.balance_hedge = self.balance + elif self.balance_delta < 0 and self.balance_delta <= -self.hedge_threshold: + self.buy(self.hedge_exchange, self.hedge_pair, -self.balance_delta, OrderType.MARKET, mid_price) + self.balance_hedge = self.balance + + def get_exchange_base_asset_balance(self): + balance_df = self.get_balance_df() + row = balance_df.iloc[0] + return Decimal(row["Total Balance"]) + + def format_status(self) -> str: + if self.wallet_balance_data_feed.is_ready(): + lines = [] + prices_str = format_df_for_printout(self.wallet_balance_data_feed.wallet_balances_df, + table_format="psql", index=True) + lines.append(f"\nWallet Data Feed:\n{prices_str}") + + precision = 3 + if self.balance_start > 0: + lines.append("\nWallets:") + lines.append(f" Starting {self.token} balance: {round(self.balance_start, precision)}") + lines.append(f" Current {self.token} balance: {round(self.balance, precision)}") + lines.append(f" Delta: {round(self.balance - self.balance_start, precision)}") + lines.append("\nExchange:") + lines.append(f" Starting {self.base} balance: {round(self.exchange_balance_start, precision)}") + lines.append(f" Current {self.base} balance: {round(self.exchange_balance, precision)}") + lines.append(f" Delta: {round(self.exchange_balance - self.exchange_balance_start, precision)}") + lines.append("\nHedge:") + lines.append(f" Threshold: {self.hedge_threshold}") + lines.append(f" Delta from last hedge: {round(self.balance_delta, precision)}") + return "\n".join(lines) + else: + return "Wallet Data Feed is not ready." diff --git a/pages/bot_orchestration/app.py b/pages/bot_orchestration/app.py index 92920a7..3461279 100644 --- a/pages/bot_orchestration/app.py +++ b/pages/bot_orchestration/app.py @@ -1,135 +1,214 @@ -import time +from glob import glob +from types import SimpleNamespace + +from commlib.exceptions import RPCClientTimeoutError + import constants -import pandas as pd import streamlit as st +from streamlit_elements import elements, mui, lazy, sync, event +import time from docker_manager import DockerManager from hbotrc import BotCommands +from ui_components.bot_performance_card import BotPerformanceCard +from ui_components.dashboard import Dashboard +from ui_components.editor import Editor +from ui_components.exited_bot_card import ExitedBotCard +from ui_components.file_explorer import FileExplorer from utils.st_utils import initialize_st_page +initialize_st_page(title="Bot Orchestration", icon="๐Ÿ™", initial_sidebar_state="collapsed") + +if "is_broker_running" not in st.session_state: + st.session_state.is_broker_running = False + +if "active_bots" not in st.session_state: + st.session_state.active_bots = {} + +if "exited_bots" not in st.session_state: + st.session_state.exited_bots = {} + +if "new_bot_name" not in st.session_state: + st.session_state.new_bot_name = "" + +if "selected_strategy" not in st.session_state: + st.session_state.selected_strategy = None + +if "selected_file" not in st.session_state: + st.session_state.selected_file = "" + +if "editor_tabs" not in st.session_state: + st.session_state.editor_tabs = {} + + +def manage_broker_container(): + if st.session_state.is_broker_running: + docker_manager.stop_container("hummingbot-broker") + with st.spinner('Stopping hummingbot broker... You are not going to be able to manage bots anymore.'): + time.sleep(5) + else: + docker_manager.create_broker() + with st.spinner('Starting hummingbot broker... This process may take a few seconds'): + time.sleep(20) + + +def launch_new_bot(): + bot_name = f"hummingbot-{st.session_state.new_bot_name.target.value}" + docker_manager.create_hummingbot_instance(instance_name=bot_name, + base_conf_folder=f"{constants.BOTS_FOLDER}/master_bot_conf/", + target_conf_folder=f"{constants.BOTS_FOLDER}/{bot_name}") + + +def update_containers_info(docker_manager): + active_containers = docker_manager.get_active_containers() + st.session_state.is_broker_running = "hummingbot-broker" in active_containers + if st.session_state.is_broker_running: + try: + active_hbot_containers = [container for container in active_containers if + "hummingbot-" in container and "broker" not in container] + previous_active_bots = st.session_state.active_bots.keys() + + # Remove bots that are no longer active + for bot in previous_active_bots: + if bot not in active_hbot_containers: + del st.session_state.active_bots[bot] + + # Add new bots + for bot in active_hbot_containers: + if bot not in previous_active_bots: + st.session_state.active_bots[bot] = { + "bot_name": bot, + "broker_client": BotCommands(host='localhost', port=1883, username='admin', password='password', + bot_id=bot) + } + + # Update bot info + for bot in st.session_state.active_bots.keys(): + try: + broker_client = st.session_state.active_bots[bot]["broker_client"] + status = broker_client.status() + history = broker_client.history() + is_running = "No strategy is currently running" not in status.msg + st.session_state.active_bots[bot]["is_running"] = is_running + st.session_state.active_bots[bot]["status"] = status.msg + st.session_state.active_bots[bot]["trades"] = history.trades + st.session_state.active_bots[bot]["selected_strategy"] = None + except RPCClientTimeoutError: + st.error(f"RPCClientTimeoutError: Could not connect to {bot}. Please review the connection.") + del st.session_state.active_bots[bot] + except RuntimeError: + st.experimental_rerun() + st.session_state.active_bots = dict( + sorted(st.session_state.active_bots.items(), key=lambda x: x[1]['is_running'], reverse=True)) + else: + st.session_state.active_bots = {} -initialize_st_page(title="Bot Orchestration", icon="๐Ÿ™") -# Start content here docker_manager = DockerManager() +CARD_WIDTH = 4 +CARD_HEIGHT = 3 -active_containers = docker_manager.get_active_containers() -exited_containers = docker_manager.get_exited_containers() +if not docker_manager.is_docker_running(): + st.warning("Docker is not running. Please start Docker and refresh the page.") + st.stop() +orchestrate, manage = st.tabs(["Orchestrate", "Manage Files"]) +update_containers_info(docker_manager) +exited_containers = [container for container in docker_manager.get_exited_containers() if "broker" not in container] +def get_grid_positions(n_cards: int, cols: int = 3, card_width: int = 4, card_height: int = 3): + rows = n_cards // cols + 1 + x_y = [(x * card_width, y * card_height) for x in range(cols) for y in range(rows)] + return sorted(x_y, key=lambda x: (x[1], x[0])) -st.write("## ๐Ÿš€Create Hummingbot Instance") -c11, c12 = st.columns([0.8, 0.2]) -with c11: - instance_name = st.text_input("Instance Name") -with c12: - st.write() - create_instance = st.button("Create Instance") - if create_instance: - bot_name = f"hummingbot-{instance_name}" - docker_manager.create_hummingbot_instance(instance_name=bot_name, - base_conf_folder=f"{constants.BOTS_FOLDER}/data_downloader/conf", - target_conf_folder=f"{constants.BOTS_FOLDER}/{bot_name}") -st.write("---") +with orchestrate: + with elements("create_bot"): + with mui.Grid(container=True, spacing=4): + with mui.Grid(item=True, xs=6): + with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True): + with mui.Grid(container=True, spacing=4): + with mui.Grid(item=True, xs=12): + mui.Typography("๐Ÿš€ Set up a new bot", variant="h3") + with mui.Grid(item=True, xs=8): + mui.TextField(label="Bot Name", variant="outlined", onChange=lazy(sync("new_bot_name")), + sx={"width": "100%"}) + with mui.Grid(item=True, xs=4): + with mui.Button(onClick=launch_new_bot): + mui.icon.AddCircleOutline() + mui.Typography("Create new bot!") + with mui.Grid(item=True, xs=6): + with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True): + with mui.Grid(container=True, spacing=4): + with mui.Grid(item=True, xs=12): + mui.Typography("๐Ÿ™ Hummingbot Broker", variant="h3") + with mui.Grid(item=True, xs=8): + mui.Typography("To control and monitor your bots you need to launch the Hummingbot Broker." + "This component will send the commands to the running bots.") + with mui.Grid(item=True, xs=4): + button_text = "Stop Broker" if st.session_state.is_broker_running else "Start Broker" + color = "error" if st.session_state.is_broker_running else "success" + icon = mui.icon.Stop if st.session_state.is_broker_running else mui.icon.PlayCircle + with mui.Button(onClick=manage_broker_container, color=color): + icon() + mui.Typography(button_text) -st.write("## ๐Ÿฆ…Hummingbot Instances") + with elements("active_instances_board"): + with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True): + mui.Typography("๐Ÿฆ… Active Instances", variant="h3") + if st.session_state.is_broker_running: + quantity_of_active_bots = len(st.session_state.active_bots) + if quantity_of_active_bots > 0: + # TODO: Make layout configurable + grid_positions = get_grid_positions(n_cards=quantity_of_active_bots, cols=3, + card_width=CARD_WIDTH, card_height=CARD_HEIGHT) + active_instances_board = Dashboard() + for (bot, config), (x, y) in zip(st.session_state.active_bots.items(), grid_positions): + st.session_state.active_bots[bot]["bot_performance_card"] = BotPerformanceCard(active_instances_board, + x, y, + CARD_WIDTH, CARD_HEIGHT) + with active_instances_board(): + for bot, config in st.session_state.active_bots.items(): + st.session_state.active_bots[bot]["bot_performance_card"](config) + else: + mui.Alert("No active bots found. Please create a new bot.", severity="info", sx={"margin": "1rem"}) + else: + mui.Alert("Please start the Hummingbot Broker to control your bots.", severity="warning", sx={"margin": "1rem"}) + with elements("stopped_instances_board"): + grid_positions = get_grid_positions(n_cards=len(exited_containers), cols=3, card_width=4, card_height=3) + exited_instances_board = Dashboard() + for exited_instance, (x, y) in zip(exited_containers, grid_positions): + st.session_state.exited_bots[exited_instance] = ExitedBotCard(exited_instances_board, x, y, + CARD_WIDTH, 1) + with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True): + mui.Typography("๐Ÿ’ค Stopped Instances", variant="h3") + with exited_instances_board(): + for bot, card in st.session_state.exited_bots.items(): + card(bot) -st.write("This section will let you control your hummingbot instances.") -c1, c2 = st.columns([0.8, 0.2]) -active_hummingbot_instances = [(container, "active") for container in active_containers if "hummingbot-" in container - and "broker" not in container] -exited_hummingbot_instances = [(container, "exited") for container in exited_containers if "hummingbot-" in container - and "broker" not in container] -all_instances = active_hummingbot_instances + exited_hummingbot_instances -if len(all_instances) > 0: - with c1: - df = pd.DataFrame(all_instances, columns=["instance_name", "status"]) - df["selected"] = False - edited_df = st.data_editor(df[["selected", "instance_name", "status"]]) - selected_instances = edited_df[edited_df["selected"]]["instance_name"].tolist() - with c2: - stop_instances = st.button("Stop Selected Instances") - start_instances = st.button("Start Selected Instances") - clean_instances = st.button("Clean Selected Instances") +with manage: + if "w" not in st.session_state: + board = Dashboard() + w = SimpleNamespace( + dashboard=board, + file_explorer=FileExplorer(board, 0, 0, 3, 7), + editor=Editor(board, 4, 0, 9, 7), + ) + st.session_state.w = w - if stop_instances: - for instance in selected_instances: - docker_manager.stop_container(instance) + else: + w = st.session_state.w - if start_instances: - for instance in selected_instances: - docker_manager.start_container(instance) + for tab_name, content in st.session_state.editor_tabs.items(): + if tab_name not in w.editor._tabs: + w.editor.add_tab(tab_name, content["content"], content["language"], content["file_path"]) - if clean_instances: - for instance in selected_instances: - docker_manager.remove_container(instance) -else: - st.info("No active hummingbot instances") - -st.write("---") -st.write("## ๐Ÿ“ฉHummingbot Broker") -if "hummingbot-broker" not in active_containers: - c1, c2 = st.columns([0.9, 0.1]) - with c1: - st.error("Hummingbot Broker is not running") - with c2: - # TODO: Add configuration variables for broker creation - create_broker = st.button("Create Hummingbot Broker") - if create_broker: - docker_manager.create_broker() -else: - c1, c2 = st.columns([0.9, 0.1]) - with c1: - st.success("Hummingbot Broker is running") - with c2: - # TODO: Make that the hummingbot client checks if the broker is running if the config is on like gateway - stop_broker = st.button("Stop Hummingbot Broker") - if stop_broker: - docker_manager.stop_container("hummingbot-broker") - if len(active_hummingbot_instances) > 0: - broker_clients = {instance_name[0]: BotCommands( - host='localhost', - port=1883, - username='admin', - password='38828943.Dardonacci', - bot_id=instance_name[0], - ) for instance_name in active_hummingbot_instances} - instance_names = [instance_name[0] for instance_name in active_hummingbot_instances] - tabs = st.tabs([instance_name for instance_name in instance_names]) - for i, tab in enumerate(tabs): - with tab: - instance_name = instance_names[i] - client = broker_clients[instance_name] - status = client.status() - bot_stopped = "No strategy is currently running" in status.msg - strategy = None - c1, c2 = st.columns([0.8, 0.2]) - with c1: - if bot_stopped: - strategy = st.text_input("Strategy config or Script to run (strategy will be the name of the config file" - "and script script_name.py)", - key=f"strategy-{instance_name}") - st.info("The bot is currently stopped. Start a strategy to get the bot status") - with c2: - if strategy: - run_strategy = st.button("Run Strategy", key=f"run-{instance_name}") - is_script = strategy.endswith(".py") - if run_strategy: - if is_script: - client.start(script=strategy) - else: - client.import_strategy(strategy=strategy.replace(".yml", "")) - time.sleep(0.5) - client.start(strategy) - status = st.button("Get Status", key=f"status-{instance_name}") - stop_strategy = st.button("Stop Strategy", key=f"stop-{instance_name}") - with c1: - if status: - status = client.status() - st.write(status.msg) - if stop_strategy: - client.stop(strategy) - st.success("Strategy stopped") + with elements("bot_config"): + with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True): + mui.Typography("๐Ÿ—‚Files Management", variant="h3", sx={"margin-bottom": "2rem"}) + event.Hotkey("ctrl+s", sync(), bindInputs=True, overrideDefault=True) + with w.dashboard(): + w.file_explorer() + w.editor() diff --git a/ui_components/__init__.py b/ui_components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui_components/bot_performance_card.py b/ui_components/bot_performance_card.py new file mode 100644 index 0000000..d5c3dee --- /dev/null +++ b/ui_components/bot_performance_card.py @@ -0,0 +1,80 @@ +from docker_manager import DockerManager +from streamlit_elements import mui, lazy +from ui_components.dashboard import Dashboard +import streamlit as st +import time +from utils.os_utils import get_python_files_from_directory, get_yml_files_from_directory + + +class BotPerformanceCard(Dashboard.Item): + + def __init__(self, board, x, y, w, h, **item_props): + super().__init__(board, x, y, w, h, **item_props) + + @staticmethod + def set_strategy(_, childs, bot_name): + st.session_state.active_bots[bot_name]["selected_strategy"] = childs.props.value + + @staticmethod + def start_strategy(bot_name, broker_client): + selected_strategy = st.session_state.active_bots[bot_name]["selected_strategy"] + if selected_strategy.endswith(".py"): + broker_client.start(script=selected_strategy) + elif selected_strategy.endswith(".yml"): + broker_client.import_strategy(strategy=selected_strategy.replace(".yml", "")) + time.sleep(0.5) + broker_client.start() + + def __call__(self, bot_config: dict): + bot_name = bot_config["bot_name"] + scripts_directory = f"./hummingbot_files/bot_configs/{bot_config['bot_name']}" + strategies_directory = f"{scripts_directory}/conf/strategies" + scripts = [file.split("/")[-1] for file in get_python_files_from_directory(scripts_directory)] + strategies = [file.split("/")[-1] for file in get_yml_files_from_directory(strategies_directory)] + if bot_config["selected_strategy"] is None: + if len(scripts): + st.session_state.active_bots[bot_name]["selected_strategy"] = scripts[0] + elif len(strategies): + st.session_state.active_bots[bot_name]["selected_strategy"] = strategies[0] + + with mui.Card(key=self._key, + sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, + elevation=2): + color = "green" if bot_config["is_running"] else "red" + subheader_message = "Running" if bot_config["is_running"] else "Stopped" + mui.CardHeader( + title=bot_config["bot_name"], + subheader=subheader_message, + avatar=mui.Avatar("๐Ÿค–", sx={"bgcolor": color}), + action=mui.IconButton(mui.icon.Stop, onClick=lambda: bot_config["broker_client"].stop()) if bot_config[ + "is_running"] else mui.IconButton(mui.icon.BuildCircle), + className=self._draggable_class, + ) + if bot_config["is_running"]: + with mui.CardContent(sx={"flex": 1}): + mui.Typography("Status:") + mui.Typography(bot_config["status"]) + mui.Typography("Trades:") + mui.Typography(str(bot_config["trades"])) + + else: + with mui.CardContent(sx={"flex": 1}): + with mui.Grid(container=True, spacing=2): + with mui.Grid(item=True, xs=12): + mui.Typography("Select a strategy:") + with mui.Grid(item=True, xs=8): + with mui.Select(onChange=lazy(lambda x, y: self.set_strategy(x, y, bot_name)), + sx={"width": "100%"}): + for script in scripts: + mui.MenuItem(script, value=script) + for strategy in strategies: + mui.MenuItem(strategy, value=strategy) + + with mui.Grid(item=True, xs=4): + with mui.Button(onClick=lambda x: self.start_strategy(bot_name, bot_config["broker_client"])): + mui.icon.PlayCircle() + mui.Typography("Start") + with mui.CardActions(disableSpacing=True): + with mui.Button(onClick=lambda: DockerManager().stop_container(bot_name)): + mui.icon.DeleteForever() + mui.Typography("Stop Container") diff --git a/ui_components/card.py b/ui_components/card.py new file mode 100644 index 0000000..10b9a90 --- /dev/null +++ b/ui_components/card.py @@ -0,0 +1,34 @@ +from streamlit_elements import mui +from ui_components.dashboard import Dashboard + + +class Card(Dashboard.Item): + + DEFAULT_CONTENT = ( + "This impressive paella is a perfect party dish and a fun meal to cook " + "together with your guests. Add 1 cup of frozen peas along with the mussels, " + "if you like." + ) + + def __call__(self, content): + with mui.Card(key=self._key, sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, elevation=1): + mui.CardHeader( + title="Shrimp and Chorizo Paella", + subheader="September 14, 2016", + avatar=mui.Avatar("R", sx={"bgcolor": "red"}), + action=mui.IconButton(mui.icon.MoreVert), + className=self._draggable_class, + ) + mui.CardMedia( + component="img", + height=194, + image="https://mui.com/static/images/cards/paella.jpg", + alt="Paella dish", + ) + + with mui.CardContent(sx={"flex": 1}): + mui.Typography(content) + + with mui.CardActions(disableSpacing=True): + mui.IconButton(mui.icon.Favorite) + mui.IconButton(mui.icon.Share) diff --git a/ui_components/dashboard.py b/ui_components/dashboard.py new file mode 100644 index 0000000..abc81a4 --- /dev/null +++ b/ui_components/dashboard.py @@ -0,0 +1,60 @@ +from uuid import uuid4 +from abc import ABC, abstractmethod +from streamlit_elements import dashboard, mui +from contextlib import contextmanager + + +class Dashboard: + + DRAGGABLE_CLASS = "draggable" + + def __init__(self): + self._layout = [] + + def _register(self, item): + self._layout.append(item) + + @contextmanager + def __call__(self, **props): + # Draggable classname query selector. + props["draggableHandle"] = f".{Dashboard.DRAGGABLE_CLASS}" + + with dashboard.Grid(self._layout, **props): + yield + + class Item(ABC): + + def __init__(self, board, x, y, w, h, **item_props): + self._key = str(uuid4()) + self._draggable_class = Dashboard.DRAGGABLE_CLASS + self._dark_mode = True + board._register(dashboard.Item(self._key, x, y, w, h, **item_props)) + + def _switch_theme(self): + self._dark_mode = not self._dark_mode + + @contextmanager + def title_bar(self, padding="5px 15px 5px 15px", dark_switcher=True): + with mui.Stack( + className=self._draggable_class, + alignItems="center", + direction="row", + spacing=1, + sx={ + "padding": padding, + "borderBottom": 1, + "borderColor": "divider", + }, + ): + yield + + if dark_switcher: + if self._dark_mode: + mui.IconButton(mui.icon.DarkMode, onClick=self._switch_theme) + else: + mui.IconButton(mui.icon.LightMode, sx={"color": "#ffc107"}, onClick=self._switch_theme) + + @abstractmethod + def __call__(self): + """Show elements.""" + raise NotImplementedError diff --git a/ui_components/datagrid.py b/ui_components/datagrid.py new file mode 100644 index 0000000..b8fbf88 --- /dev/null +++ b/ui_components/datagrid.py @@ -0,0 +1,51 @@ +import json + +from streamlit_elements import mui +from .dashboard import Dashboard + + +class DataGrid(Dashboard.Item): + DEFAULT_COLUMNS = [ + {"field": 'id', "headerName": 'ID', "width": 90}, + {"field": 'firstName', "headerName": 'First name', "width": 150, "editable": True, }, + {"field": 'lastName', "headerName": 'Last name', "width": 150, "editable": True, }, + {"field": 'age', "headerName": 'Age', "type": 'number', "width": 110, "editable": True, }, + ] + DEFAULT_ROWS = [ + {"id": 1, "lastName": 'Snow', "firstName": 'Jon', "age": 35}, + {"id": 2, "lastName": 'Lannister', "firstName": 'Cersei', "age": 42}, + {"id": 3, "lastName": 'Lannister', "firstName": 'Jaime', "age": 45}, + {"id": 4, "lastName": 'Stark', "firstName": 'Arya', "age": 16}, + {"id": 5, "lastName": 'Targaryen', "firstName": 'Daenerys', "age": None}, + {"id": 6, "lastName": 'Melisandre', "firstName": None, "age": 150}, + {"id": 7, "lastName": 'Clifford', "firstName": 'Ferrara', "age": 44}, + {"id": 8, "lastName": 'Frances', "firstName": 'Rossini', "age": 36}, + {"id": 9, "lastName": 'Roxie', "firstName": 'Harvey', "age": 65}, + ] + + def _handle_edit(self, params): + print(params) + + def __call__(self, json_data): + try: + data = json.loads(json_data) + except json.JSONDecodeError: + data = self.DEFAULT_ROWS + + with mui.Paper(key=self._key, + sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, + elevation=1): + with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False): + mui.icon.ViewCompact() + mui.Typography("Data grid") + + with mui.Box(sx={"flex": 1, "minHeight": 0}): + mui.DataGrid( + columns=self.DEFAULT_COLUMNS, + rows=data, + pageSize=5, + rowsPerPageOptions=[5], + checkboxSelection=True, + disableSelectionOnClick=True, + onCellEditCommit=self._handle_edit, + ) diff --git a/ui_components/editor.py b/ui_components/editor.py new file mode 100644 index 0000000..6ba14a1 --- /dev/null +++ b/ui_components/editor.py @@ -0,0 +1,88 @@ +from functools import partial +import streamlit as st +from streamlit_elements import mui, editor, sync, lazy + +from utils.os_utils import save_file +from .dashboard import Dashboard + + +class Editor(Dashboard.Item): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._dark_theme = False + self._index = 0 + self._tabs = {} + self._editor_box_style = { + "flex": 1, + "minHeight": 0, + "borderBottom": 1, + "borderTop": 1, + "borderColor": "divider" + } + + def save_file(self): + if len(self._tabs) > 0: + label = list(self._tabs.keys())[self._index] + content = self.get_content(label) + full_path = self.get_file_path(label) + file_name = full_path.split("/")[-1] + path = "/".join(full_path.split("/")[:-1]) + save_file(name=file_name, content=content, path=path) + st.info("File saved") + + def _change_tab(self, _, index): + self._index = index + + def update_content(self, label, content): + self._tabs[label]["content"] = content + + def add_tab(self, label, default_content, language, file_path): + self._tabs[label] = { + "content": default_content, + "language": language, + "file_path": file_path + } + + def get_content(self, label): + return self._tabs[label]["content"] + + def get_file_path(self, label): + return self._tabs[label]["file_path"] + + def __call__(self): + with mui.Paper(key=self._key, sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, elevation=1): + + with self.title_bar("0px 15px 0px 15px"): + with mui.Grid(container=True, spacing=4, sx={"display": "flex", "alignItems": "center"}): + with mui.Grid(item=True, xs=10, sx={"display": "flex", "alignItems": "center"}): + mui.icon.Terminal() + mui.Typography("Editor") + with mui.Tabs(value=self._index, onChange=self._change_tab, scrollButtons=True, + variant="scrollable", sx={"flex": 1}): + for label in self._tabs.keys(): + mui.Tab(label=label) + with mui.Grid(item=True, xs=2, sx={"display": "flex", "justifyContent": "flex-end"}): + mui.IconButton(mui.icon.Save, onClick=self.save_file, sx={"mx": 1}) + + + + for index, (label, tab) in enumerate(self._tabs.items()): + with mui.Box(sx=self._editor_box_style, hidden=(index != self._index)): + editor.Monaco( + css={"padding": "0 2px 0 2px"}, + defaultValue=tab["content"], + language=tab["language"], + onChange=lazy(partial(self.update_content, label)), + theme="vs-dark" if self._dark_mode else "light", + path=label, + options={ + "wordWrap": True, + "fontSize": 16.5, + } + ) + + with mui.Stack(direction="row", spacing=2, alignItems="center", sx={"padding": "10px"}): + mui.Button("Apply", variant="contained", onClick=sync()) + mui.Typography("Or press ctrl+s", sx={"flex": 1}) diff --git a/ui_components/exited_bot_card.py b/ui_components/exited_bot_card.py new file mode 100644 index 0000000..5b9f11d --- /dev/null +++ b/ui_components/exited_bot_card.py @@ -0,0 +1,38 @@ +from docker_manager import DockerManager +from streamlit_elements import mui, lazy +from ui_components.dashboard import Dashboard +import streamlit as st +import time + +from utils import os_utils +from utils.os_utils import get_python_files_from_directory, get_yml_files_from_directory + + +class ExitedBotCard(Dashboard.Item): + + def __init__(self, board, x, y, w, h, **item_props): + super().__init__(board, x, y, w, h, **item_props) + + @staticmethod + def remove_container(bot_name): + DockerManager().remove_container(bot_name) + os_utils.remove_directory(f"./hummingbot_files/bot_configs/{bot_name}") + + def __call__(self, bot_name: str): + with mui.Card(key=self._key, + sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, + elevation=2): + mui.CardHeader( + title=bot_name, + subheader="Stopped", + avatar=mui.Avatar("๐Ÿ’€", sx={"bgcolor": "grey"}), + className=self._draggable_class, + ) + + with mui.CardActions(disableSpacing=True): + with mui.Button(onClick=lambda: DockerManager().start_container(bot_name), color="success"): + mui.icon.PlayCircle() + mui.Typography("Start Container") + with mui.Button(onClick=lambda: self.remove_container(bot_name), color="error"): + mui.icon.DeleteForever() + mui.Typography("Delete Container") diff --git a/ui_components/file_explorer.py b/ui_components/file_explorer.py new file mode 100644 index 0000000..fc5e88e --- /dev/null +++ b/ui_components/file_explorer.py @@ -0,0 +1,55 @@ +import streamlit as st +from streamlit_elements import mui, elements + +from utils.os_utils import get_directories_from_directory, get_python_files_from_directory, \ + get_yml_files_from_directory, load_file, remove_file +from .dashboard import Dashboard + + +class FileExplorer(Dashboard.Item): + _directory = "hummingbot_files/bot_configs" + + @staticmethod + def set_selected_file(_, node_id): + st.session_state.selected_file = node_id + + @staticmethod + def delete_file(): + if st.session_state.selected_file: + if st.session_state.selected_file.endswith(".py") or st.session_state.selected_file.endswith(".yml"): + remove_file(st.session_state.selected_file) + else: + st.error("You can't delete the directory since it's a volume." + "If you want to do it, go to the orchestrate tab and delete the container") + + def edit_file(self): + short_path = st.session_state.selected_file.replace(self._directory, "") + language = "python" if st.session_state.selected_file.endswith(".py") else "yaml" + if st.session_state.selected_file.endswith(".py") or st.session_state.selected_file.endswith(".yml"): + st.session_state.editor_tabs[short_path] = {"content": load_file(st.session_state.selected_file), + "language": language, + "file_path": st.session_state.selected_file} + + def __call__(self): + bots = [bot.split("/")[-2] for bot in get_directories_from_directory(self._directory) if + "data_downloader" not in bot] + with mui.Paper(key=self._key, + sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, + elevation=1): + with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False): + with mui.Grid(container=True, spacing=4, sx={"display": "flex", "alignItems": "center"}): + with mui.Grid(item=True, xs=6, sx={"display": "flex", "alignItems": "center"}): + mui.icon.Folder() + mui.Typography("File Explorer") + with mui.Grid(item=True, xs=6, sx={"display": "flex", "justifyContent": "flex-end"}): + mui.IconButton(mui.icon.Delete, onClick=self.delete_file, sx={"mx": 1}) + mui.IconButton(mui.icon.Edit, onClick=self.edit_file, sx={"mx": 1}) + with mui.Box(sx={"overflow": "auto"}): + with mui.lab.TreeView(defaultExpandIcon=mui.icon.ChevronRight, defaultCollapseIcon=mui.icon.ExpandMore, + onNodeSelect=lambda event, node_id: self.set_selected_file(event, node_id)): + for bot in bots: + with mui.lab.TreeItem(nodeId=bot, label=f"๐Ÿค–{bot}"): + for file in get_python_files_from_directory(f"{self._directory}/{bot}/scripts"): + mui.lab.TreeItem(nodeId=file, label=f"๐Ÿ{file.split('/')[-1]}") + for file in get_yml_files_from_directory(f"{self._directory}/{bot}/conf/strategies"): + mui.lab.TreeItem(nodeId=file, label=f"๐Ÿ“„ {file.split('/')[-1]}") diff --git a/ui_components/media_player.py b/ui_components/media_player.py new file mode 100644 index 0000000..6faf78e --- /dev/null +++ b/ui_components/media_player.py @@ -0,0 +1,23 @@ +from streamlit_elements import media, mui, sync, lazy +from .dashboard import Dashboard + +class Player(Dashboard.Item): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._url = "https://www.youtube.com/watch?v=CmSKVW1v0xM" + + def _set_url(self, event): + self._url = event.target.value + + def __call__(self): + with mui.Paper(key=self._key, sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"}, elevation=1): + with self.title_bar(padding="10px 15px 10px 15px", dark_switcher=False): + mui.icon.OndemandVideo() + mui.Typography("Media player") + + with mui.Stack(direction="row", spacing=2, justifyContent="space-evenly", alignItems="center", sx={"padding": "10px"}): + mui.TextField(defaultValue=self._url, label="URL", variant="standard", sx={"flex": 0.97}, onChange=lazy(self._set_url)) + mui.IconButton(mui.icon.PlayCircleFilled, onClick=sync(), sx={"color": "primary.main"}) + + media.Player(self._url, controls=True, width="100%", height="100%") diff --git a/utils/os_utils.py b/utils/os_utils.py index 9918ab7..32dc999 100644 --- a/utils/os_utils.py +++ b/utils/os_utils.py @@ -14,11 +14,14 @@ def remove_files_from_directory(directory: str): for file in os.listdir(directory): os.remove(f"{directory}/{file}") +def remove_file(file_path: str): + os.remove(file_path) def remove_directory(directory: str): process = subprocess.Popen(f"rm -rf {directory}", shell=True) process.wait() + def dump_dict_to_yaml(data_dict, filename): with open(filename, 'w') as file: yaml.dump(data_dict, file) @@ -54,12 +57,22 @@ def load_file(path: str) -> str: return "" +def get_directories_from_directory(directory: str) -> list: + directories = glob.glob(directory + "/**/") + return directories + + def get_python_files_from_directory(directory: str) -> list: py_files = glob.glob(directory + "/**/*.py", recursive=True) py_files = [path for path in py_files if not path.endswith("__init__.py")] return py_files +def get_yml_files_from_directory(directory: str) -> list: + yml = glob.glob(directory + "/**/*.yml", recursive=True) + return yml + + def load_directional_strategies(path): strategies = {} for filename in os.listdir(path): diff --git a/utils/st_utils.py b/utils/st_utils.py index 36e545b..bdfdf72 100644 --- a/utils/st_utils.py +++ b/utils/st_utils.py @@ -5,7 +5,7 @@ import inspect import streamlit as st -def initialize_st_page(title: str, icon: str, layout="wide", initial_sidebar_state="collapsed"): +def initialize_st_page(title: str, icon: str, layout="wide", initial_sidebar_state="auto"): st.set_page_config( page_title=title, page_icon=icon,