Merge pull request #50 from hummingbot/feat/bot_orchestration_v2

Feat/bot orchestration v2
This commit is contained in:
dardonacci
2023-08-02 21:28:33 +02:00
committed by GitHub
57 changed files with 6900 additions and 114 deletions

View File

@@ -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

View File

@@ -0,0 +1 @@
7b2263727970746f223a207b22636970686572223a20226165732d3132382d637472222c2022636970686572706172616d73223a207b226976223a20223864336365306436393461623131396334363135663935366464653839363063227d2c202263697068657274657874223a20223836333266323430613563306131623665353664222c20226b6466223a202270626b646632222c20226b6466706172616d73223a207b2263223a20313030303030302c2022646b6c656e223a2033322c2022707266223a2022686d61632d736861323536222c202273616c74223a20226566373330376531636464373964376132303338323534656139343433663930227d2c20226d6163223a202266393439383534613530633138363633386363353962336133363665633962353333386633613964373266636635343066313034333361353431636232306438227d2c202276657273696f6e223a20337d

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

@@ -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."

View File

@@ -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']}")

View File

@@ -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

View File

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

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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]

View File

@@ -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"}')

View File

@@ -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()

View File

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

View File

@@ -0,0 +1,5 @@
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
class CLOBSerumExample(ScriptStrategyBase):
pass

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

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

View File

@@ -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

View File

@@ -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')}")

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

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

View File

@@ -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."

View File

@@ -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()

View File

View File

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

34
ui_components/card.py Normal file
View File

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

View File

@@ -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

51
ui_components/datagrid.py Normal file
View File

@@ -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,
)

88
ui_components/editor.py Normal file
View File

@@ -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})

View File

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

View File

@@ -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]}")

View File

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

View File

@@ -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):

View File

@@ -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,