mirror of
https://github.com/aljazceru/hummingbot-dashboard.git
synced 2026-01-10 08:54:23 +01:00
Merge pull request #74 from hummingbot/feat/add_base_controlers_and_master_script
Feat/add base controlers and master script
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
CANDLES_DATA_PATH = "data/candles"
|
||||
DOWNLOAD_CANDLES_CONFIG_YML = "hummingbot_files/scripts_configs/data_downloader_config.yml"
|
||||
BOTS_FOLDER = "hummingbot_files/bot_configs"
|
||||
BOTS_FOLDER = "hummingbot_files/bots"
|
||||
CONTROLLERS_PATH = "quants_lab/controllers"
|
||||
CONTROLLERS_CONFIG_PATH = "hummingbot_files/controller_configs"
|
||||
OPTIMIZATIONS_PATH = "quants_lab/optimizations"
|
||||
|
||||
@@ -5,8 +5,8 @@ services:
|
||||
image: hummingbot/hummingbot:development
|
||||
volumes:
|
||||
- "../../data/candles:/home/hummingbot/data"
|
||||
- "../bot_configs/data_downloader/conf:/home/hummingbot/conf"
|
||||
- "../bot_configs/data_downloader/conf/connectors:/home/hummingbot/conf/connectors"
|
||||
- "../bots/data_downloader/conf:/home/hummingbot/conf"
|
||||
- "../bots/data_downloader/conf/connectors:/home/hummingbot/conf/connectors"
|
||||
environment:
|
||||
- CONFIG_PASSWORD=a
|
||||
- CONFIG_FILE_NAME=download_candles.py
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
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}")
|
||||
@@ -1,147 +0,0 @@
|
||||
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)
|
||||
@@ -1,48 +0,0 @@
|
||||
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."
|
||||
@@ -1,49 +0,0 @@
|
||||
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']}")
|
||||
@@ -1,120 +0,0 @@
|
||||
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
|
||||
@@ -1,181 +0,0 @@
|
||||
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)
|
||||
@@ -1,122 +0,0 @@
|
||||
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,
|
||||
)
|
||||
@@ -1,104 +0,0 @@
|
||||
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,
|
||||
)
|
||||
@@ -1,130 +0,0 @@
|
||||
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]
|
||||
@@ -1,58 +0,0 @@
|
||||
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"}')
|
||||
@@ -1,43 +0,0 @@
|
||||
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()
|
||||
@@ -1,91 +0,0 @@
|
||||
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)
|
||||
@@ -1,5 +0,0 @@
|
||||
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
|
||||
|
||||
|
||||
class CLOBSerumExample(ScriptStrategyBase):
|
||||
pass
|
||||
@@ -1,77 +0,0 @@
|
||||
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")
|
||||
@@ -1,120 +0,0 @@
|
||||
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
|
||||
@@ -1,98 +0,0 @@
|
||||
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
|
||||
@@ -1,98 +0,0 @@
|
||||
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
|
||||
@@ -1,97 +0,0 @@
|
||||
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
|
||||
@@ -1,78 +0,0 @@
|
||||
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
|
||||
@@ -1,73 +0,0 @@
|
||||
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
|
||||
@@ -1,101 +0,0 @@
|
||||
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
|
||||
@@ -4,7 +4,7 @@ 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.data_feed.candles_feed.candles_factory import CandlesConfig, CandlesFactory
|
||||
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
|
||||
|
||||
|
||||
@@ -37,9 +37,8 @@ class DownloadCandles(ScriptStrategyBase):
|
||||
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 = CandlesFactory.get_candle(CandlesConfig(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
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
|
||||
from hummingbot import data_path
|
||||
from hummingbot.connector.connector_base import ConnectorBase
|
||||
from hummingbot.core.event.event_forwarder import SourceInfoEventForwarder
|
||||
from hummingbot.core.event.events import OrderBookEvent, OrderBookTradeEvent
|
||||
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
|
||||
|
||||
|
||||
class DownloadTradesAndOrderBookSnapshots(ScriptStrategyBase):
|
||||
exchange = os.getenv("EXCHANGE", "binance_paper_trade")
|
||||
trading_pairs = os.getenv("TRADING_PAIRS", "ETH-USDT,BTC-USDT")
|
||||
depth = int(os.getenv("DEPTH", 50))
|
||||
trading_pairs = [pair for pair in trading_pairs.split(",")]
|
||||
last_dump_timestamp = 0
|
||||
time_between_csv_dumps = 10
|
||||
|
||||
ob_temp_storage = {trading_pair: [] for trading_pair in trading_pairs}
|
||||
trades_temp_storage = {trading_pair: [] for trading_pair in trading_pairs}
|
||||
current_date = None
|
||||
ob_file_paths = {}
|
||||
trades_file_paths = {}
|
||||
markets = {exchange: set(trading_pairs)}
|
||||
subscribed_to_order_book_trade_event: bool = False
|
||||
|
||||
def __init__(self, connectors: Dict[str, ConnectorBase]):
|
||||
super().__init__(connectors)
|
||||
self.create_order_book_and_trade_files()
|
||||
self.order_book_trade_event = SourceInfoEventForwarder(self._process_public_trade)
|
||||
|
||||
def on_tick(self):
|
||||
if not self.subscribed_to_order_book_trade_event:
|
||||
self.subscribe_to_order_book_trade_event()
|
||||
self.check_and_replace_files()
|
||||
for trading_pair in self.trading_pairs:
|
||||
order_book_data = self.get_order_book_dict(self.exchange, trading_pair, self.depth)
|
||||
self.ob_temp_storage[trading_pair].append(order_book_data)
|
||||
if self.last_dump_timestamp < self.current_timestamp:
|
||||
self.dump_and_clean_temp_storage()
|
||||
|
||||
def get_order_book_dict(self, exchange: str, trading_pair: str, depth: int = 50):
|
||||
order_book = self.connectors[exchange].get_order_book(trading_pair)
|
||||
snapshot = order_book.snapshot
|
||||
return {
|
||||
"ts": self.current_timestamp,
|
||||
"bids": snapshot[0].loc[:(depth - 1), ["price", "amount"]].values.tolist(),
|
||||
"asks": snapshot[1].loc[:(depth - 1), ["price", "amount"]].values.tolist(),
|
||||
}
|
||||
|
||||
def dump_and_clean_temp_storage(self):
|
||||
for trading_pair, order_book_info in self.ob_temp_storage.items():
|
||||
file = self.ob_file_paths[trading_pair]
|
||||
json_strings = [json.dumps(obj) for obj in order_book_info]
|
||||
json_data = '\n'.join(json_strings)
|
||||
file.write(json_data)
|
||||
self.ob_temp_storage[trading_pair] = []
|
||||
for trading_pair, trades_info in self.trades_temp_storage.items():
|
||||
file = self.trades_file_paths[trading_pair]
|
||||
json_strings = [json.dumps(obj) for obj in trades_info]
|
||||
json_data = '\n'.join(json_strings)
|
||||
file.write(json_data)
|
||||
self.trades_temp_storage[trading_pair] = []
|
||||
self.last_dump_timestamp = self.current_timestamp + self.time_between_csv_dumps
|
||||
|
||||
def check_and_replace_files(self):
|
||||
current_date = datetime.now().strftime("%Y-%m-%d")
|
||||
if current_date != self.current_date:
|
||||
for file in self.ob_file_paths.values():
|
||||
file.close()
|
||||
self.create_order_book_and_trade_files()
|
||||
|
||||
def create_order_book_and_trade_files(self):
|
||||
self.current_date = datetime.now().strftime("%Y-%m-%d")
|
||||
self.ob_file_paths = {trading_pair: self.get_file(self.exchange, trading_pair, "order_book_snapshots", self.current_date) for
|
||||
trading_pair in self.trading_pairs}
|
||||
self.trades_file_paths = {trading_pair: self.get_file(self.exchange, trading_pair, "trades", self.current_date) for
|
||||
trading_pair in self.trading_pairs}
|
||||
|
||||
@staticmethod
|
||||
def get_file(exchange: str, trading_pair: str, source_type: str, current_date: str):
|
||||
file_path = data_path() + f"/{exchange}_{trading_pair}_{source_type}_{current_date}.txt"
|
||||
return open(file_path, "a")
|
||||
|
||||
def _process_public_trade(self, event_tag: int, market: ConnectorBase, event: OrderBookTradeEvent):
|
||||
self.trades_temp_storage[event.trading_pair].append({
|
||||
"ts": event.timestamp,
|
||||
"price": event.price,
|
||||
"q_base": event.amount,
|
||||
"side": event.type.name.lower(),
|
||||
})
|
||||
|
||||
def subscribe_to_order_book_trade_event(self):
|
||||
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
|
||||
@@ -1,74 +0,0 @@
|
||||
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")
|
||||
341
hummingbot_files/templates/master_bot_conf/scripts/fixed_grid.py
Normal file
341
hummingbot_files/templates/master_bot_conf/scripts/fixed_grid.py
Normal file
@@ -0,0 +1,341 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from typing import Dict, List
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
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.core.utils import map_df_to_str
|
||||
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
|
||||
|
||||
|
||||
class FixedGrid(ScriptStrategyBase):
|
||||
# Parameters to modify -----------------------------------------
|
||||
trading_pair = "ENJ-USDT"
|
||||
exchange = "ascend_ex"
|
||||
n_levels = 8
|
||||
grid_price_ceiling = Decimal(0.33)
|
||||
grid_price_floor = Decimal(0.3)
|
||||
order_amount = Decimal(18.0)
|
||||
# Optional ----------------------
|
||||
spread_scale_factor = Decimal(1.0)
|
||||
amount_scale_factor = Decimal(1.0)
|
||||
rebalance_order_type = "limit"
|
||||
rebalance_order_spread = Decimal(0.02)
|
||||
rebalance_order_refresh_time = 60.0
|
||||
grid_orders_refresh_time = 3600000.0
|
||||
price_source = PriceType.MidPrice
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
markets = {exchange: {trading_pair}}
|
||||
create_timestamp = 0
|
||||
price_levels = []
|
||||
base_inv_levels = []
|
||||
quote_inv_levels = []
|
||||
order_amount_levels = []
|
||||
quote_inv_levels_current_price = []
|
||||
current_level = -100
|
||||
grid_spread = (grid_price_ceiling - grid_price_floor) / (n_levels - 1)
|
||||
inv_correct = True
|
||||
rebalance_order_amount = Decimal(0.0)
|
||||
rebalance_order_buy = True
|
||||
|
||||
def __init__(self, connectors: Dict[str, ConnectorBase]):
|
||||
super().__init__(connectors)
|
||||
|
||||
self.minimum_spread = (self.grid_price_ceiling - self.grid_price_floor) / (1 + 2 * sum([pow(self.spread_scale_factor, n) for n in range(1, int(self.n_levels / 2))]))
|
||||
self.price_levels.append(self.grid_price_floor)
|
||||
for i in range(2, int(self.n_levels / 2) + 1):
|
||||
price = self.grid_price_floor + self.minimum_spread * sum([pow(self.spread_scale_factor, int(self.n_levels / 2) - n) for n in range(1, i)])
|
||||
self.price_levels.append(price)
|
||||
for i in range(1, int(self.n_levels / 2) + 1):
|
||||
self.order_amount_levels.append(self.order_amount * pow(self.amount_scale_factor, int(self.n_levels / 2) - i))
|
||||
|
||||
for i in range(int(self.n_levels / 2) + 1, self.n_levels + 1):
|
||||
price = self.price_levels[int(self.n_levels / 2) - 1] + self.minimum_spread * sum([pow(self.spread_scale_factor, n) for n in range(0, i - int(self.n_levels / 2))])
|
||||
self.price_levels.append(price)
|
||||
self.order_amount_levels.append(self.order_amount * pow(self.amount_scale_factor, i - int(self.n_levels / 2) - 1))
|
||||
|
||||
for i in range(1, self.n_levels + 1):
|
||||
self.base_inv_levels.append(sum(self.order_amount_levels[i:self.n_levels]))
|
||||
self.quote_inv_levels.append(sum([self.price_levels[n] * self.order_amount_levels[n] for n in range(0, i - 1)]))
|
||||
for i in range(self.n_levels):
|
||||
self.quote_inv_levels_current_price.append(self.quote_inv_levels[i] / self.price_levels[i])
|
||||
|
||||
def on_tick(self):
|
||||
proposal = None
|
||||
if self.create_timestamp <= self.current_timestamp:
|
||||
# If grid level not yet set, find it.
|
||||
if self.current_level == -100:
|
||||
price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source)
|
||||
# Find level closest to market
|
||||
min_diff = 1e8
|
||||
for i in range(self.n_levels):
|
||||
if min(min_diff, abs(self.price_levels[i] - price)) < min_diff:
|
||||
min_diff = abs(self.price_levels[i] - price)
|
||||
self.current_level = i
|
||||
|
||||
msg = (f"Current price {price}, Initial level {self.current_level+1}")
|
||||
self.log_with_clock(logging.INFO, msg)
|
||||
self.notify_hb_app_with_timestamp(msg)
|
||||
|
||||
if price > self.grid_price_ceiling:
|
||||
msg = ("WARNING: Current price is above grid ceiling")
|
||||
self.log_with_clock(logging.WARNING, msg)
|
||||
self.notify_hb_app_with_timestamp(msg)
|
||||
elif price < self.grid_price_floor:
|
||||
msg = ("WARNING: Current price is below grid floor")
|
||||
self.log_with_clock(logging.WARNING, msg)
|
||||
self.notify_hb_app_with_timestamp(msg)
|
||||
|
||||
market, trading_pair, base_asset, quote_asset = self.get_market_trading_pair_tuples()[0]
|
||||
base_balance = float(market.get_balance(base_asset))
|
||||
quote_balance = float(market.get_balance(quote_asset) / self.price_levels[self.current_level])
|
||||
|
||||
if base_balance < self.base_inv_levels[self.current_level]:
|
||||
self.inv_correct = False
|
||||
msg = (f"WARNING: Insuffient {base_asset} balance for grid bot. Will attempt to rebalance")
|
||||
self.log_with_clock(logging.WARNING, msg)
|
||||
self.notify_hb_app_with_timestamp(msg)
|
||||
if base_balance + quote_balance < self.base_inv_levels[self.current_level] + self.quote_inv_levels_current_price[self.current_level]:
|
||||
msg = (f"WARNING: Insuffient {base_asset} and {quote_asset} balance for grid bot. Unable to rebalance."
|
||||
f"Please add funds or change grid parameters")
|
||||
self.log_with_clock(logging.WARNING, msg)
|
||||
self.notify_hb_app_with_timestamp(msg)
|
||||
return
|
||||
else:
|
||||
# Calculate additional base required with 5% tolerance
|
||||
base_required = (Decimal(self.base_inv_levels[self.current_level]) - Decimal(base_balance)) * Decimal(1.05)
|
||||
self.rebalance_order_buy = True
|
||||
self.rebalance_order_amount = Decimal(base_required)
|
||||
elif quote_balance < self.quote_inv_levels_current_price[self.current_level]:
|
||||
self.inv_correct = False
|
||||
msg = (f"WARNING: Insuffient {quote_asset} balance for grid bot. Will attempt to rebalance")
|
||||
self.log_with_clock(logging.WARNING, msg)
|
||||
self.notify_hb_app_with_timestamp(msg)
|
||||
if base_balance + quote_balance < self.base_inv_levels[self.current_level] + self.quote_inv_levels_current_price[self.current_level]:
|
||||
msg = (f"WARNING: Insuffient {base_asset} and {quote_asset} balance for grid bot. Unable to rebalance."
|
||||
f"Please add funds or change grid parameters")
|
||||
self.log_with_clock(logging.WARNING, msg)
|
||||
self.notify_hb_app_with_timestamp(msg)
|
||||
return
|
||||
else:
|
||||
# Calculate additional quote required with 5% tolerance
|
||||
quote_required = (Decimal(self.quote_inv_levels_current_price[self.current_level]) - Decimal(quote_balance)) * Decimal(1.05)
|
||||
self.rebalance_order_buy = False
|
||||
self.rebalance_order_amount = Decimal(quote_required)
|
||||
else:
|
||||
self.inv_correct = True
|
||||
|
||||
if self.inv_correct is True:
|
||||
# Create proposals for Grid
|
||||
proposal = self.create_grid_proposal()
|
||||
else:
|
||||
# Create rebalance proposal
|
||||
proposal = self.create_rebalance_proposal()
|
||||
|
||||
self.cancel_active_orders()
|
||||
if proposal is not None:
|
||||
self.execute_orders_proposal(proposal)
|
||||
|
||||
def create_grid_proposal(self) -> List[OrderCandidate]:
|
||||
buys = []
|
||||
sells = []
|
||||
|
||||
# Proposal will be created according to grid price levels
|
||||
for i in range(self.current_level):
|
||||
price = self.price_levels[i]
|
||||
size = self.order_amount_levels[i]
|
||||
if size > 0:
|
||||
buy_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
|
||||
order_side=TradeType.BUY, amount=size, price=price)
|
||||
buys.append(buy_order)
|
||||
|
||||
for i in range(self.current_level + 1, self.n_levels):
|
||||
price = self.price_levels[i]
|
||||
size = self.order_amount_levels[i]
|
||||
if size > 0:
|
||||
sell_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
|
||||
order_side=TradeType.SELL, amount=size, price=price)
|
||||
sells.append(sell_order)
|
||||
|
||||
return buys + sells
|
||||
|
||||
def create_rebalance_proposal(self):
|
||||
buys = []
|
||||
sells = []
|
||||
|
||||
# Proposal will be created according to start order spread.
|
||||
if self.rebalance_order_buy is True:
|
||||
ref_price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source)
|
||||
price = ref_price * (Decimal("100") - self.rebalance_order_spread) / Decimal("100")
|
||||
size = self.rebalance_order_amount
|
||||
|
||||
msg = (f"Placing buy order to rebalance; amount: {size}, price: {price}")
|
||||
self.log_with_clock(logging.INFO, msg)
|
||||
self.notify_hb_app_with_timestamp(msg)
|
||||
if size > 0:
|
||||
if self.rebalance_order_type == "limit":
|
||||
buy_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
|
||||
order_side=TradeType.BUY, amount=size, price=price)
|
||||
elif self.rebalance_order_type == "market":
|
||||
buy_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.MARKET,
|
||||
order_side=TradeType.BUY, amount=size, price=price)
|
||||
buys.append(buy_order)
|
||||
|
||||
if self.rebalance_order_buy is False:
|
||||
ref_price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source)
|
||||
price = ref_price * (Decimal("100") + self.rebalance_order_spread) / Decimal("100")
|
||||
size = self.rebalance_order_amount
|
||||
msg = (f"Placing sell order to rebalance; amount: {size}, price: {price}")
|
||||
self.log_with_clock(logging.INFO, msg)
|
||||
self.notify_hb_app_with_timestamp(msg)
|
||||
if size > 0:
|
||||
if self.rebalance_order_type == "limit":
|
||||
sell_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
|
||||
order_side=TradeType.SELL, amount=size, price=price)
|
||||
elif self.rebalance_order_type == "market":
|
||||
sell_order = OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.MARKET,
|
||||
order_side=TradeType.SELL, amount=size, price=price)
|
||||
sells.append(sell_order)
|
||||
|
||||
return buys + sells
|
||||
|
||||
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)
|
||||
|
||||
def did_complete_buy_order(self, event: BuyOrderCompletedEvent):
|
||||
if self.inv_correct is False:
|
||||
self.create_timestamp = self.current_timestamp + float(1.0)
|
||||
|
||||
if self.inv_correct is True:
|
||||
# Set the new level
|
||||
self.current_level -= 1
|
||||
# Add sell order above current level
|
||||
price = self.price_levels[self.current_level + 1]
|
||||
size = self.order_amount_levels[self.current_level + 1]
|
||||
proposal = [OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
|
||||
order_side=TradeType.SELL, amount=size, price=price)]
|
||||
self.execute_orders_proposal(proposal)
|
||||
|
||||
def did_complete_sell_order(self, event: SellOrderCompletedEvent):
|
||||
if self.inv_correct is False:
|
||||
self.create_timestamp = self.current_timestamp + float(1.0)
|
||||
|
||||
if self.inv_correct is True:
|
||||
# Set the new level
|
||||
self.current_level += 1
|
||||
# Add buy order above current level
|
||||
price = self.price_levels[self.current_level - 1]
|
||||
size = self.order_amount_levels[self.current_level - 1]
|
||||
proposal = [OrderCandidate(trading_pair=self.trading_pair, is_maker=True, order_type=OrderType.LIMIT,
|
||||
order_side=TradeType.BUY, amount=size, price=price)]
|
||||
self.execute_orders_proposal(proposal)
|
||||
|
||||
def execute_orders_proposal(self, proposal: List[OrderCandidate]) -> None:
|
||||
for order in proposal:
|
||||
self.place_order(connector_name=self.exchange, order=order)
|
||||
if self.inv_correct is False:
|
||||
next_cycle = self.current_timestamp + self.rebalance_order_refresh_time
|
||||
if self.create_timestamp <= self.current_timestamp:
|
||||
self.create_timestamp = next_cycle
|
||||
else:
|
||||
next_cycle = self.current_timestamp + self.grid_orders_refresh_time
|
||||
if self.create_timestamp <= self.current_timestamp:
|
||||
self.create_timestamp = next_cycle
|
||||
|
||||
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 grid_assets_df(self) -> pd.DataFrame:
|
||||
market, trading_pair, base_asset, quote_asset = self.get_market_trading_pair_tuples()[0]
|
||||
price = self.connectors[self.exchange].get_price_by_type(self.trading_pair, self.price_source)
|
||||
base_balance = float(market.get_balance(base_asset))
|
||||
quote_balance = float(market.get_balance(quote_asset))
|
||||
available_base_balance = float(market.get_available_balance(base_asset))
|
||||
available_quote_balance = float(market.get_available_balance(quote_asset))
|
||||
base_value = base_balance * float(price)
|
||||
total_in_quote = base_value + quote_balance
|
||||
base_ratio = base_value / total_in_quote if total_in_quote > 0 else 0
|
||||
quote_ratio = quote_balance / total_in_quote if total_in_quote > 0 else 0
|
||||
data = [
|
||||
["", base_asset, quote_asset],
|
||||
["Total Balance", round(base_balance, 4), round(quote_balance, 4)],
|
||||
["Available Balance", round(available_base_balance, 4), round(available_quote_balance, 4)],
|
||||
[f"Current Value ({quote_asset})", round(base_value, 4), round(quote_balance, 4)]
|
||||
]
|
||||
data.append(["Current %", f"{base_ratio:.1%}", f"{quote_ratio:.1%}"])
|
||||
df = pd.DataFrame(data=data)
|
||||
return df
|
||||
|
||||
def grid_status_data_frame(self) -> pd.DataFrame:
|
||||
grid_data = []
|
||||
grid_columns = ["Parameter", "Value"]
|
||||
|
||||
market, trading_pair, base_asset, quote_asset = self.get_market_trading_pair_tuples()[0]
|
||||
base_balance = float(market.get_balance(base_asset))
|
||||
quote_balance = float(market.get_balance(quote_asset) / self.price_levels[self.current_level])
|
||||
|
||||
grid_data.append(["Grid spread", round(self.grid_spread, 4)])
|
||||
grid_data.append(["Current grid level", self.current_level + 1])
|
||||
grid_data.append([f"{base_asset} required", round(self.base_inv_levels[self.current_level], 4)])
|
||||
grid_data.append([f"{quote_asset} required in {base_asset}", round(self.quote_inv_levels_current_price[self.current_level], 4)])
|
||||
grid_data.append([f"{base_asset} balance", round(base_balance, 4)])
|
||||
grid_data.append([f"{quote_asset} balance in {base_asset}", round(quote_balance, 4)])
|
||||
grid_data.append(["Correct inventory balance", self.inv_correct])
|
||||
|
||||
return pd.DataFrame(data=grid_data, columns=grid_columns).replace(np.nan, '', regex=True)
|
||||
|
||||
def format_status(self) -> str:
|
||||
"""
|
||||
Displays the status of the fixed grid strategy
|
||||
Returns status of the current strategy on user balances and current active orders.
|
||||
"""
|
||||
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")])
|
||||
|
||||
grid_df = map_df_to_str(self.grid_status_data_frame())
|
||||
lines.extend(["", " Grid:"] + [" " + line for line in grid_df.to_string(index=False).split("\n")])
|
||||
|
||||
assets_df = map_df_to_str(self.grid_assets_df())
|
||||
|
||||
first_col_length = max(*assets_df[0].apply(len))
|
||||
df_lines = assets_df.to_string(index=False, header=False,
|
||||
formatters={0: ("{:<" + str(first_col_length) + "}").format}).split("\n")
|
||||
lines.extend(["", " Assets:"] + [" " + line for line in df_lines])
|
||||
|
||||
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)
|
||||
|
||||
def cancel_active_orders(self):
|
||||
"""
|
||||
Cancels active orders
|
||||
"""
|
||||
for order in self.get_active_orders(connector_name=self.exchange):
|
||||
self.cancel(self.exchange, order.trading_pair, order.client_order_id)
|
||||
@@ -1,47 +0,0 @@
|
||||
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
|
||||
@@ -1,19 +0,0 @@
|
||||
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')}")
|
||||
@@ -1,235 +0,0 @@
|
||||
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
|
||||
@@ -1,18 +0,0 @@
|
||||
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)
|
||||
@@ -1,305 +0,0 @@
|
||||
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
|
||||
@@ -1,342 +0,0 @@
|
||||
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
|
||||
@@ -1,174 +0,0 @@
|
||||
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)
|
||||
@@ -1,492 +0,0 @@
|
||||
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."
|
||||
)
|
||||
@@ -1,199 +0,0 @@
|
||||
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
|
||||
@@ -2,7 +2,7 @@ import inspect
|
||||
import os
|
||||
import importlib.util
|
||||
|
||||
from hummingbot.core.data_type.common import OrderType, PositionMode, TradeType
|
||||
from hummingbot.core.data_type.common import OrderType, PositionMode, TradeType, PositionSide, PositionAction
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import (
|
||||
ExecutorHandlerStatus,
|
||||
)
|
||||
@@ -59,9 +59,37 @@ class StrategyV2Launcher(ScriptStrategyBase):
|
||||
self.executor_handlers[controller_config] = DirectionalTradingExecutorHandler(strategy=self, controller=controller)
|
||||
|
||||
def on_stop(self):
|
||||
for connector in self.connectors.keys():
|
||||
if self.is_perpetual(connector):
|
||||
self.close_open_positions(connector)
|
||||
for executor_handler in self.executor_handlers.values():
|
||||
executor_handler.stop()
|
||||
|
||||
@staticmethod
|
||||
def is_perpetual(exchange):
|
||||
"""
|
||||
Checks if the exchange is a perpetual market.
|
||||
"""
|
||||
return "perpetual" in exchange
|
||||
|
||||
def close_open_positions(self, exchange):
|
||||
connector = self.connectors[exchange]
|
||||
for trading_pair, position in connector.account_positions.items():
|
||||
if position.position_side == PositionSide.LONG:
|
||||
self.sell(connector_name=exchange,
|
||||
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=exchange,
|
||||
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 on_tick(self):
|
||||
"""
|
||||
This shows you how you can start meta controllers. You can run more than one at the same time and based on the
|
||||
@@ -76,6 +104,7 @@ class StrategyV2Launcher(ScriptStrategyBase):
|
||||
return "Market connectors are not ready."
|
||||
lines = []
|
||||
for controller_config, executor_handler in self.executor_handlers.items():
|
||||
lines.extend(["\n------------------------------------------------------------------------------------------"])
|
||||
lines.extend([f"Strategy: {executor_handler.controller.config.strategy_name} | Config: {controller_config}",
|
||||
executor_handler.to_format_status()])
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
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)
|
||||
@@ -0,0 +1,91 @@
|
||||
from decimal import Decimal
|
||||
from typing import Dict
|
||||
|
||||
from hummingbot.connector.connector_base import ConnectorBase, TradeType
|
||||
from hummingbot.core.data_type.common import OrderType
|
||||
from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
|
||||
from hummingbot.smart_components.controllers.macd_bb_v1 import MACDBBV1, MACDBBV1Config
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import (
|
||||
ExecutorHandlerStatus,
|
||||
OrderLevel,
|
||||
TripleBarrierConf,
|
||||
)
|
||||
from hummingbot.smart_components.strategy_frameworks.directional_trading.directional_trading_executor_handler import (
|
||||
DirectionalTradingExecutorHandler,
|
||||
)
|
||||
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
|
||||
|
||||
|
||||
class MarketMakingDmanComposed(ScriptStrategyBase):
|
||||
trading_pairs = ["HBAR-USDT", "CYBER-USDT", "ETH-USDT", "LPT-USDT", "UNFI-USDT"]
|
||||
leverage_by_trading_pair = {
|
||||
"HBAR-USDT": 25,
|
||||
"CYBER-USDT": 20,
|
||||
"ETH-USDT": 100,
|
||||
"LPT-USDT": 10,
|
||||
"UNFI-USDT": 20,
|
||||
}
|
||||
triple_barrier_conf = TripleBarrierConf(
|
||||
stop_loss=Decimal("0.01"), take_profit=Decimal("0.03"),
|
||||
time_limit=60 * 60 * 6,
|
||||
trailing_stop_activation_price_delta=Decimal("0.008"),
|
||||
trailing_stop_trailing_delta=Decimal("0.004"),
|
||||
open_order_type=OrderType.MARKET
|
||||
)
|
||||
|
||||
order_levels = [
|
||||
OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal("15"),
|
||||
spread_factor=Decimal(0.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal("15"),
|
||||
spread_factor=Decimal(0.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
]
|
||||
controllers = {}
|
||||
markets = {}
|
||||
executor_handlers = {}
|
||||
|
||||
for trading_pair in trading_pairs:
|
||||
config = MACDBBV1Config(
|
||||
exchange="binance_perpetual",
|
||||
trading_pair=trading_pair,
|
||||
order_levels=order_levels,
|
||||
candles_config=[
|
||||
CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=100),
|
||||
],
|
||||
leverage=leverage_by_trading_pair[trading_pair],
|
||||
macd_fast=21, macd_slow=42, macd_signal=9,
|
||||
bb_length=100, bb_std=2.0, bb_long_threshold=0.3, bb_short_threshold=0.7,
|
||||
)
|
||||
controller = MACDBBV1(config=config)
|
||||
markets = controller.update_strategy_markets_dict(markets)
|
||||
controllers[trading_pair] = controller
|
||||
|
||||
def __init__(self, connectors: Dict[str, ConnectorBase]):
|
||||
super().__init__(connectors)
|
||||
for trading_pair, controller in self.controllers.items():
|
||||
self.executor_handlers[trading_pair] = DirectionalTradingExecutorHandler(strategy=self, controller=controller)
|
||||
|
||||
def on_stop(self):
|
||||
for executor_handler in self.executor_handlers.values():
|
||||
executor_handler.stop()
|
||||
|
||||
def on_tick(self):
|
||||
"""
|
||||
This shows you how you can start meta controllers. You can run more than one at the same time and based on the
|
||||
market conditions, you can orchestrate from this script when to stop or start them.
|
||||
"""
|
||||
for executor_handler in self.executor_handlers.values():
|
||||
if executor_handler.status == ExecutorHandlerStatus.NOT_STARTED:
|
||||
executor_handler.start()
|
||||
|
||||
def format_status(self) -> str:
|
||||
if not self.ready_to_trade:
|
||||
return "Market connectors are not ready."
|
||||
lines = []
|
||||
for trading_pair, executor_handler in self.executor_handlers.items():
|
||||
if executor_handler.controller.all_candles_ready:
|
||||
lines.extend(
|
||||
[f"Strategy: {executor_handler.controller.config.strategy_name} | Trading Pair: {trading_pair}",
|
||||
executor_handler.to_format_status()])
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1,153 @@
|
||||
from decimal import Decimal
|
||||
from typing import Dict
|
||||
|
||||
from hummingbot.connector.connector_base import ConnectorBase, TradeType
|
||||
from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide
|
||||
from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
|
||||
from hummingbot.smart_components.controllers.bollingrid import BollingGrid, BollingGridConfig
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import (
|
||||
ExecutorHandlerStatus,
|
||||
OrderLevel,
|
||||
TripleBarrierConf,
|
||||
)
|
||||
from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import (
|
||||
MarketMakingExecutorHandler,
|
||||
)
|
||||
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
|
||||
|
||||
|
||||
class BollinGridMultiplePairs(ScriptStrategyBase):
|
||||
trading_pairs = ["RUNE-USDT", "AGLD-USDT"]
|
||||
exchange = "binance_perpetual"
|
||||
|
||||
# This is only for the perpetual markets
|
||||
leverage_by_trading_pair = {
|
||||
"HBAR-USDT": 25,
|
||||
"CYBER-USDT": 20,
|
||||
"ETH-USDT": 100,
|
||||
"LPT-USDT": 10,
|
||||
"UNFI-USDT": 20,
|
||||
"BAKE-USDT": 20,
|
||||
"YGG-USDT": 20,
|
||||
"SUI-USDT": 50,
|
||||
"TOMO-USDT": 25,
|
||||
"RUNE-USDT": 25,
|
||||
"STX-USDT": 25,
|
||||
"API3-USDT": 20,
|
||||
"LIT-USDT": 20,
|
||||
"PERP-USDT": 16,
|
||||
"HOOK-USDT": 20,
|
||||
"AMB-USDT": 20,
|
||||
"ARKM-USDT": 20,
|
||||
"TRB-USDT": 10,
|
||||
"OMG-USDT": 25,
|
||||
"WLD-USDT": 50,
|
||||
"PEOPLE-USDT": 25,
|
||||
"AGLD-USDT": 20,
|
||||
"BAT-USDT": 20
|
||||
}
|
||||
|
||||
triple_barrier_conf = TripleBarrierConf(
|
||||
stop_loss=Decimal("0.15"), take_profit=Decimal("0.02"),
|
||||
time_limit=60 * 60 * 12,
|
||||
take_profit_order_type=OrderType.LIMIT,
|
||||
trailing_stop_activation_price_delta=Decimal("0.005"),
|
||||
trailing_stop_trailing_delta=Decimal("0.002"),
|
||||
)
|
||||
|
||||
order_levels = [
|
||||
OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal("10"),
|
||||
spread_factor=Decimal(0.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=2, side=TradeType.BUY, order_amount_usd=Decimal("20"),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=3, side=TradeType.BUY, order_amount_usd=Decimal("30"),
|
||||
spread_factor=Decimal(1.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
|
||||
OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal("10"),
|
||||
spread_factor=Decimal(0.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=2, side=TradeType.SELL, order_amount_usd=Decimal("20"),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=3, side=TradeType.SELL, order_amount_usd=Decimal("30"),
|
||||
spread_factor=Decimal(1.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
]
|
||||
controllers = {}
|
||||
markets = {}
|
||||
executor_handlers = {}
|
||||
|
||||
for trading_pair in trading_pairs:
|
||||
config = BollingGridConfig(
|
||||
exchange=exchange,
|
||||
trading_pair=trading_pair,
|
||||
order_levels=order_levels,
|
||||
candles_config=[
|
||||
CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="15m", max_records=300),
|
||||
],
|
||||
bb_length=200,
|
||||
bb_std=3.0,
|
||||
leverage=leverage_by_trading_pair.get(trading_pair, 1),
|
||||
)
|
||||
controller = BollingGrid(config=config)
|
||||
markets = controller.update_strategy_markets_dict(markets)
|
||||
controllers[trading_pair] = controller
|
||||
|
||||
def __init__(self, connectors: Dict[str, ConnectorBase]):
|
||||
super().__init__(connectors)
|
||||
for trading_pair, controller in self.controllers.items():
|
||||
self.executor_handlers[trading_pair] = MarketMakingExecutorHandler(strategy=self, controller=controller)
|
||||
|
||||
@property
|
||||
def is_perpetual(self):
|
||||
"""
|
||||
Checks if the exchange is a perpetual market.
|
||||
"""
|
||||
return "perpetual" in self.exchange
|
||||
|
||||
def on_stop(self):
|
||||
if self.is_perpetual:
|
||||
self.close_open_positions()
|
||||
for executor_handler in self.executor_handlers.values():
|
||||
executor_handler.stop()
|
||||
|
||||
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 on_tick(self):
|
||||
"""
|
||||
This shows you how you can start meta controllers. You can run more than one at the same time and based on the
|
||||
market conditions, you can orchestrate from this script when to stop or start them.
|
||||
"""
|
||||
for executor_handler in self.executor_handlers.values():
|
||||
if executor_handler.status == ExecutorHandlerStatus.NOT_STARTED:
|
||||
executor_handler.start()
|
||||
|
||||
def format_status(self) -> str:
|
||||
if not self.ready_to_trade:
|
||||
return "Market connectors are not ready."
|
||||
lines = []
|
||||
for trading_pair, executor_handler in self.executor_handlers.items():
|
||||
lines.extend(
|
||||
[f"Strategy: {executor_handler.controller.config.strategy_name} | Trading Pair: {trading_pair}",
|
||||
executor_handler.to_format_status()])
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1,145 @@
|
||||
from decimal import Decimal
|
||||
from typing import Dict
|
||||
|
||||
from hummingbot.connector.connector_base import ConnectorBase, TradeType
|
||||
from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide
|
||||
from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
|
||||
from hummingbot.smart_components.controllers.dman_v1 import DManV1, DManV1Config
|
||||
from hummingbot.smart_components.controllers.dman_v2 import DManV2, DManV2Config
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import (
|
||||
ExecutorHandlerStatus,
|
||||
OrderLevel,
|
||||
TripleBarrierConf,
|
||||
)
|
||||
from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import (
|
||||
MarketMakingExecutorHandler,
|
||||
)
|
||||
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
|
||||
|
||||
|
||||
class MarketMakingDmanComposed(ScriptStrategyBase):
|
||||
trading_pair = "HBAR-USDT"
|
||||
triple_barrier_conf_top = TripleBarrierConf(
|
||||
stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"),
|
||||
time_limit=60 * 60 * 1,
|
||||
trailing_stop_activation_price_delta=Decimal("0.002"),
|
||||
trailing_stop_trailing_delta=Decimal("0.0005")
|
||||
)
|
||||
triple_barrier_conf_bottom = TripleBarrierConf(
|
||||
stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"),
|
||||
time_limit=60 * 60 * 3,
|
||||
trailing_stop_activation_price_delta=Decimal("0.005"),
|
||||
trailing_stop_trailing_delta=Decimal("0.001")
|
||||
)
|
||||
|
||||
config_v1 = DManV1Config(
|
||||
exchange="binance_perpetual",
|
||||
trading_pair=trading_pair,
|
||||
order_levels=[
|
||||
OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal(15),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 30,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top),
|
||||
OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal(50),
|
||||
spread_factor=Decimal(5.0), order_refresh_time=60 * 30,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
|
||||
OrderLevel(level=2, side=TradeType.BUY, order_amount_usd=Decimal(50),
|
||||
spread_factor=Decimal(8.0), order_refresh_time=60 * 15,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
|
||||
OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal(15),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 30,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top),
|
||||
OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal(50),
|
||||
spread_factor=Decimal(5.0), order_refresh_time=60 * 30,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
|
||||
OrderLevel(level=2, side=TradeType.SELL, order_amount_usd=Decimal(50),
|
||||
spread_factor=Decimal(8.0), order_refresh_time=60 * 15,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
|
||||
],
|
||||
candles_config=[
|
||||
CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=1000),
|
||||
],
|
||||
leverage=25,
|
||||
natr_length=21
|
||||
)
|
||||
config_v2 = DManV2Config(
|
||||
exchange="binance_perpetual",
|
||||
trading_pair=trading_pair,
|
||||
order_levels=[
|
||||
OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal(15),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top),
|
||||
OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal(30),
|
||||
spread_factor=Decimal(2.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
|
||||
OrderLevel(level=2, side=TradeType.BUY, order_amount_usd=Decimal(50),
|
||||
spread_factor=Decimal(3.0), order_refresh_time=60 * 15,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
|
||||
OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal(15),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf_top),
|
||||
OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal(30),
|
||||
spread_factor=Decimal(2.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
|
||||
OrderLevel(level=2, side=TradeType.SELL, order_amount_usd=Decimal(50),
|
||||
spread_factor=Decimal(3.0), order_refresh_time=60 * 15,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf_bottom),
|
||||
],
|
||||
candles_config=[
|
||||
CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=1000),
|
||||
],
|
||||
leverage=25,
|
||||
natr_length=21, macd_fast=12, macd_slow=26, macd_signal=9
|
||||
)
|
||||
dman_v1 = DManV1(config=config_v1)
|
||||
dman_v2 = DManV2(config=config_v2)
|
||||
|
||||
empty_markets = {}
|
||||
markets = dman_v1.update_strategy_markets_dict(empty_markets)
|
||||
markets = dman_v2.update_strategy_markets_dict(markets)
|
||||
|
||||
def __init__(self, connectors: Dict[str, ConnectorBase]):
|
||||
super().__init__(connectors)
|
||||
self.dman_v1_executor = MarketMakingExecutorHandler(strategy=self, controller=self.dman_v1)
|
||||
self.dman_v2_executor = MarketMakingExecutorHandler(strategy=self, controller=self.dman_v2)
|
||||
|
||||
def on_stop(self):
|
||||
self.close_open_positions()
|
||||
|
||||
def on_tick(self):
|
||||
"""
|
||||
This shows you how you can start meta controllers. You can run more than one at the same time and based on the
|
||||
market conditions, you can orchestrate from this script when to stop or start them.
|
||||
"""
|
||||
if self.dman_v1_executor.status == ExecutorHandlerStatus.NOT_STARTED:
|
||||
self.dman_v1_executor.start()
|
||||
if self.dman_v2_executor.status == ExecutorHandlerStatus.NOT_STARTED:
|
||||
self.dman_v2_executor.start()
|
||||
|
||||
def format_status(self) -> str:
|
||||
if not self.ready_to_trade:
|
||||
return "Market connectors are not ready."
|
||||
lines = []
|
||||
lines.extend(["DMAN V1", self.dman_v1_executor.to_format_status()])
|
||||
lines.extend(["\n-----------------------------------------\n"])
|
||||
lines.extend(["DMAN V2", self.dman_v2_executor.to_format_status()])
|
||||
return "\n".join(lines)
|
||||
|
||||
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 trading_pair in self.markets[connector_name]:
|
||||
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)
|
||||
@@ -0,0 +1,98 @@
|
||||
from decimal import Decimal
|
||||
from typing import Dict
|
||||
|
||||
from hummingbot.connector.connector_base import ConnectorBase, TradeType
|
||||
from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide
|
||||
from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
|
||||
from hummingbot.smart_components.controllers.dman_v1 import DManV1, DManV1Config
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import (
|
||||
ExecutorHandlerStatus,
|
||||
OrderLevel,
|
||||
TripleBarrierConf,
|
||||
)
|
||||
from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import (
|
||||
MarketMakingExecutorHandler,
|
||||
)
|
||||
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
|
||||
|
||||
|
||||
class MarketMakingDmanV1(ScriptStrategyBase):
|
||||
trading_pair = "HBAR-USDT"
|
||||
triple_barrier_conf = TripleBarrierConf(
|
||||
stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"),
|
||||
time_limit=60 * 60 * 24,
|
||||
trailing_stop_activation_price_delta=Decimal("0.002"),
|
||||
trailing_stop_trailing_delta=Decimal("0.0005")
|
||||
)
|
||||
|
||||
config_v1 = DManV1Config(
|
||||
exchange="binance_perpetual",
|
||||
trading_pair=trading_pair,
|
||||
order_levels=[
|
||||
OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal(20),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal(50),
|
||||
spread_factor=Decimal(2.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal(20),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal(50),
|
||||
spread_factor=Decimal(2.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
],
|
||||
candles_config=[
|
||||
CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=1000),
|
||||
],
|
||||
leverage=10,
|
||||
natr_length=21
|
||||
)
|
||||
|
||||
dman_v1 = DManV1(config=config_v1)
|
||||
|
||||
empty_markets = {}
|
||||
markets = dman_v1.update_strategy_markets_dict(empty_markets)
|
||||
|
||||
def __init__(self, connectors: Dict[str, ConnectorBase]):
|
||||
super().__init__(connectors)
|
||||
self.dman_v1_executor = MarketMakingExecutorHandler(strategy=self, controller=self.dman_v1)
|
||||
|
||||
def on_stop(self):
|
||||
self.close_open_positions()
|
||||
|
||||
def on_tick(self):
|
||||
"""
|
||||
This shows you how you can start meta controllers. You can run more than one at the same time and based on the
|
||||
market conditions, you can orchestrate from this script when to stop or start them.
|
||||
"""
|
||||
if self.dman_v1_executor.status == ExecutorHandlerStatus.NOT_STARTED:
|
||||
self.dman_v1_executor.start()
|
||||
|
||||
def format_status(self) -> str:
|
||||
if not self.ready_to_trade:
|
||||
return "Market connectors are not ready."
|
||||
lines = []
|
||||
lines.extend(["DMAN V1", self.dman_v1_executor.to_format_status()])
|
||||
lines.extend(["\n-----------------------------------------\n"])
|
||||
return "\n".join(lines)
|
||||
|
||||
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 trading_pair in self.markets[connector_name]:
|
||||
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)
|
||||
@@ -0,0 +1,102 @@
|
||||
from decimal import Decimal
|
||||
from typing import Dict
|
||||
|
||||
from hummingbot.connector.connector_base import ConnectorBase, TradeType
|
||||
from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide
|
||||
from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
|
||||
from hummingbot.smart_components.controllers.dman_v2 import DManV2, DManV2Config
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import (
|
||||
ExecutorHandlerStatus,
|
||||
OrderLevel,
|
||||
TripleBarrierConf,
|
||||
)
|
||||
from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import (
|
||||
MarketMakingExecutorHandler,
|
||||
)
|
||||
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
|
||||
|
||||
|
||||
class MarketMakingDmanV2(ScriptStrategyBase):
|
||||
trading_pair = "HBAR-USDT"
|
||||
triple_barrier_conf = TripleBarrierConf(
|
||||
stop_loss=Decimal("0.03"), take_profit=Decimal("0.02"),
|
||||
time_limit=60 * 60 * 24,
|
||||
trailing_stop_activation_price_delta=Decimal("0.002"),
|
||||
trailing_stop_trailing_delta=Decimal("0.0005")
|
||||
)
|
||||
config_v2 = DManV2Config(
|
||||
exchange="binance_perpetual",
|
||||
trading_pair=trading_pair,
|
||||
order_levels=[
|
||||
OrderLevel(level=0, side=TradeType.BUY, order_amount_usd=Decimal(15),
|
||||
spread_factor=Decimal(0.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal(30),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=2, side=TradeType.BUY, order_amount_usd=Decimal(50),
|
||||
spread_factor=Decimal(2.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=0, side=TradeType.SELL, order_amount_usd=Decimal(15),
|
||||
spread_factor=Decimal(0.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal(30),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=2, side=TradeType.SELL, order_amount_usd=Decimal(50),
|
||||
spread_factor=Decimal(2.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
],
|
||||
candles_config=[
|
||||
CandlesConfig(connector="binance_perpetual", trading_pair=trading_pair, interval="3m", max_records=1000),
|
||||
],
|
||||
leverage=10,
|
||||
natr_length=21, macd_fast=12, macd_slow=26, macd_signal=9
|
||||
)
|
||||
dman_v2 = DManV2(config=config_v2)
|
||||
|
||||
empty_markets = {}
|
||||
markets = dman_v2.update_strategy_markets_dict(empty_markets)
|
||||
|
||||
def __init__(self, connectors: Dict[str, ConnectorBase]):
|
||||
super().__init__(connectors)
|
||||
self.dman_v2_executor = MarketMakingExecutorHandler(strategy=self, controller=self.dman_v2)
|
||||
|
||||
def on_stop(self):
|
||||
self.close_open_positions()
|
||||
|
||||
def on_tick(self):
|
||||
"""
|
||||
This shows you how you can start meta controllers. You can run more than one at the same time and based on the
|
||||
market conditions, you can orchestrate from this script when to stop or start them.
|
||||
"""
|
||||
if self.dman_v2_executor.status == ExecutorHandlerStatus.NOT_STARTED:
|
||||
self.dman_v2_executor.start()
|
||||
|
||||
def format_status(self) -> str:
|
||||
if not self.ready_to_trade:
|
||||
return "Market connectors are not ready."
|
||||
lines = []
|
||||
lines.extend(["DMAN V2", self.dman_v2_executor.to_format_status()])
|
||||
lines.extend(["\n-----------------------------------------\n"])
|
||||
return "\n".join(lines)
|
||||
|
||||
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 trading_pair in self.markets[connector_name]:
|
||||
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)
|
||||
@@ -0,0 +1,153 @@
|
||||
from decimal import Decimal
|
||||
from typing import Dict
|
||||
|
||||
from hummingbot.connector.connector_base import ConnectorBase, TradeType
|
||||
from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide
|
||||
from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
|
||||
from hummingbot.smart_components.controllers.dman_v2 import DManV2, DManV2Config
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import (
|
||||
ExecutorHandlerStatus,
|
||||
OrderLevel,
|
||||
TripleBarrierConf,
|
||||
)
|
||||
from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import (
|
||||
MarketMakingExecutorHandler,
|
||||
)
|
||||
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
|
||||
|
||||
|
||||
class DManV2MultiplePairs(ScriptStrategyBase):
|
||||
trading_pairs = ["RUNE-USDT", "AGLD-USDT"]
|
||||
exchange = "binance_perpetual"
|
||||
|
||||
# This is only for the perpetual markets
|
||||
leverage_by_trading_pair = {
|
||||
"HBAR-USDT": 25,
|
||||
"CYBER-USDT": 20,
|
||||
"ETH-USDT": 100,
|
||||
"LPT-USDT": 10,
|
||||
"UNFI-USDT": 20,
|
||||
"BAKE-USDT": 20,
|
||||
"YGG-USDT": 20,
|
||||
"SUI-USDT": 50,
|
||||
"TOMO-USDT": 25,
|
||||
"RUNE-USDT": 25,
|
||||
"STX-USDT": 25,
|
||||
"API3-USDT": 20,
|
||||
"LIT-USDT": 20,
|
||||
"PERP-USDT": 16,
|
||||
"HOOK-USDT": 20,
|
||||
"AMB-USDT": 20,
|
||||
"ARKM-USDT": 20,
|
||||
"TRB-USDT": 10,
|
||||
"OMG-USDT": 25,
|
||||
"WLD-USDT": 50,
|
||||
"PEOPLE-USDT": 25,
|
||||
"AGLD-USDT": 20,
|
||||
"BAT-USDT": 20
|
||||
}
|
||||
|
||||
triple_barrier_conf = TripleBarrierConf(
|
||||
stop_loss=Decimal("0.15"), take_profit=Decimal("0.02"),
|
||||
time_limit=60 * 60 * 12,
|
||||
take_profit_order_type=OrderType.LIMIT,
|
||||
trailing_stop_activation_price_delta=Decimal("0.005"),
|
||||
trailing_stop_trailing_delta=Decimal("0.002"),
|
||||
)
|
||||
|
||||
order_levels = [
|
||||
OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal("10"),
|
||||
spread_factor=Decimal(0.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=2, side=TradeType.BUY, order_amount_usd=Decimal("20"),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=3, side=TradeType.BUY, order_amount_usd=Decimal("30"),
|
||||
spread_factor=Decimal(1.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
|
||||
OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal("10"),
|
||||
spread_factor=Decimal(0.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=2, side=TradeType.SELL, order_amount_usd=Decimal("20"),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=3, side=TradeType.SELL, order_amount_usd=Decimal("30"),
|
||||
spread_factor=Decimal(1.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
]
|
||||
controllers = {}
|
||||
markets = {}
|
||||
executor_handlers = {}
|
||||
|
||||
for trading_pair in trading_pairs:
|
||||
config = DManV2Config(
|
||||
exchange=exchange,
|
||||
trading_pair=trading_pair,
|
||||
order_levels=order_levels,
|
||||
candles_config=[
|
||||
CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="15m", max_records=300),
|
||||
],
|
||||
macd_fast=21, macd_slow=42, macd_signal=9,
|
||||
natr_length=100,
|
||||
leverage=leverage_by_trading_pair.get(trading_pair, 1),
|
||||
)
|
||||
controller = DManV2(config=config)
|
||||
markets = controller.update_strategy_markets_dict(markets)
|
||||
controllers[trading_pair] = controller
|
||||
|
||||
def __init__(self, connectors: Dict[str, ConnectorBase]):
|
||||
super().__init__(connectors)
|
||||
for trading_pair, controller in self.controllers.items():
|
||||
self.executor_handlers[trading_pair] = MarketMakingExecutorHandler(strategy=self, controller=controller)
|
||||
|
||||
@property
|
||||
def is_perpetual(self):
|
||||
"""
|
||||
Checks if the exchange is a perpetual market.
|
||||
"""
|
||||
return "perpetual" in self.exchange
|
||||
|
||||
def on_stop(self):
|
||||
if self.is_perpetual:
|
||||
self.close_open_positions()
|
||||
for executor_handler in self.executor_handlers.values():
|
||||
executor_handler.stop()
|
||||
|
||||
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 on_tick(self):
|
||||
"""
|
||||
This shows you how you can start meta controllers. You can run more than one at the same time and based on the
|
||||
market conditions, you can orchestrate from this script when to stop or start them.
|
||||
"""
|
||||
for executor_handler in self.executor_handlers.values():
|
||||
if executor_handler.status == ExecutorHandlerStatus.NOT_STARTED:
|
||||
executor_handler.start()
|
||||
|
||||
def format_status(self) -> str:
|
||||
if not self.ready_to_trade:
|
||||
return "Market connectors are not ready."
|
||||
lines = []
|
||||
for trading_pair, executor_handler in self.executor_handlers.items():
|
||||
lines.extend(
|
||||
[f"Strategy: {executor_handler.controller.config.strategy_name} | Trading Pair: {trading_pair}",
|
||||
executor_handler.to_format_status()])
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1,153 @@
|
||||
from decimal import Decimal
|
||||
from typing import Dict
|
||||
|
||||
from hummingbot.connector.connector_base import ConnectorBase, TradeType
|
||||
from hummingbot.core.data_type.common import OrderType, PositionAction, PositionSide
|
||||
from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
|
||||
from hummingbot.smart_components.controllers.dman_v3 import DManV3, DManV3Config
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import (
|
||||
ExecutorHandlerStatus,
|
||||
OrderLevel,
|
||||
TripleBarrierConf,
|
||||
)
|
||||
from hummingbot.smart_components.strategy_frameworks.market_making.market_making_executor_handler import (
|
||||
MarketMakingExecutorHandler,
|
||||
)
|
||||
from hummingbot.strategy.script_strategy_base import ScriptStrategyBase
|
||||
|
||||
|
||||
class DManV3MultiplePairs(ScriptStrategyBase):
|
||||
trading_pairs = ["RUNE-USDT", "AGLD-USDT"]
|
||||
exchange = "binance_perpetual"
|
||||
|
||||
# This is only for the perpetual markets
|
||||
leverage_by_trading_pair = {
|
||||
"HBAR-USDT": 25,
|
||||
"CYBER-USDT": 20,
|
||||
"ETH-USDT": 100,
|
||||
"LPT-USDT": 10,
|
||||
"UNFI-USDT": 20,
|
||||
"BAKE-USDT": 20,
|
||||
"YGG-USDT": 20,
|
||||
"SUI-USDT": 50,
|
||||
"TOMO-USDT": 25,
|
||||
"RUNE-USDT": 25,
|
||||
"STX-USDT": 25,
|
||||
"API3-USDT": 20,
|
||||
"LIT-USDT": 20,
|
||||
"PERP-USDT": 16,
|
||||
"HOOK-USDT": 20,
|
||||
"AMB-USDT": 20,
|
||||
"ARKM-USDT": 20,
|
||||
"TRB-USDT": 10,
|
||||
"OMG-USDT": 25,
|
||||
"WLD-USDT": 50,
|
||||
"PEOPLE-USDT": 25,
|
||||
"AGLD-USDT": 20,
|
||||
"BAT-USDT": 20
|
||||
}
|
||||
|
||||
triple_barrier_conf = TripleBarrierConf(
|
||||
stop_loss=Decimal("0.15"), take_profit=Decimal("0.02"),
|
||||
time_limit=60 * 60 * 12,
|
||||
take_profit_order_type=OrderType.LIMIT,
|
||||
trailing_stop_activation_price_delta=Decimal("0.005"),
|
||||
trailing_stop_trailing_delta=Decimal("0.002"),
|
||||
)
|
||||
|
||||
order_levels = [
|
||||
OrderLevel(level=1, side=TradeType.BUY, order_amount_usd=Decimal("10"),
|
||||
spread_factor=Decimal(0.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=2, side=TradeType.BUY, order_amount_usd=Decimal("20"),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=3, side=TradeType.BUY, order_amount_usd=Decimal("30"),
|
||||
spread_factor=Decimal(1.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
|
||||
OrderLevel(level=1, side=TradeType.SELL, order_amount_usd=Decimal("10"),
|
||||
spread_factor=Decimal(0.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=2, side=TradeType.SELL, order_amount_usd=Decimal("20"),
|
||||
spread_factor=Decimal(1.0), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
OrderLevel(level=3, side=TradeType.SELL, order_amount_usd=Decimal("30"),
|
||||
spread_factor=Decimal(1.5), order_refresh_time=60 * 5,
|
||||
cooldown_time=15, triple_barrier_conf=triple_barrier_conf),
|
||||
]
|
||||
controllers = {}
|
||||
markets = {}
|
||||
executor_handlers = {}
|
||||
|
||||
for trading_pair in trading_pairs:
|
||||
config = DManV3Config(
|
||||
exchange=exchange,
|
||||
trading_pair=trading_pair,
|
||||
order_levels=order_levels,
|
||||
candles_config=[
|
||||
CandlesConfig(connector=exchange, trading_pair=trading_pair, interval="15m", max_records=300),
|
||||
],
|
||||
bb_length=200,
|
||||
bb_std=3.0,
|
||||
leverage=leverage_by_trading_pair.get(trading_pair, 1),
|
||||
)
|
||||
controller = DManV3(config=config)
|
||||
markets = controller.update_strategy_markets_dict(markets)
|
||||
controllers[trading_pair] = controller
|
||||
|
||||
def __init__(self, connectors: Dict[str, ConnectorBase]):
|
||||
super().__init__(connectors)
|
||||
for trading_pair, controller in self.controllers.items():
|
||||
self.executor_handlers[trading_pair] = MarketMakingExecutorHandler(strategy=self, controller=controller)
|
||||
|
||||
@property
|
||||
def is_perpetual(self):
|
||||
"""
|
||||
Checks if the exchange is a perpetual market.
|
||||
"""
|
||||
return "perpetual" in self.exchange
|
||||
|
||||
def on_stop(self):
|
||||
if self.is_perpetual:
|
||||
self.close_open_positions()
|
||||
for executor_handler in self.executor_handlers.values():
|
||||
executor_handler.stop()
|
||||
|
||||
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 on_tick(self):
|
||||
"""
|
||||
This shows you how you can start meta controllers. You can run more than one at the same time and based on the
|
||||
market conditions, you can orchestrate from this script when to stop or start them.
|
||||
"""
|
||||
for executor_handler in self.executor_handlers.values():
|
||||
if executor_handler.status == ExecutorHandlerStatus.NOT_STARTED:
|
||||
executor_handler.start()
|
||||
|
||||
def format_status(self) -> str:
|
||||
if not self.ready_to_trade:
|
||||
return "Market connectors are not ready."
|
||||
lines = []
|
||||
for trading_pair, executor_handler in self.executor_handlers.items():
|
||||
lines.extend(
|
||||
[f"Strategy: {executor_handler.controller.config.strategy_name} | Trading Pair: {trading_pair}",
|
||||
executor_handler.to_format_status()])
|
||||
return "\n".join(lines)
|
||||
@@ -1,90 +0,0 @@
|
||||
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."
|
||||
7
main.py
7
main.py
@@ -27,16 +27,15 @@ if st.session_state["authentication_status"]:
|
||||
Page("main.py", "Hummingbot Dashboard", "📊"),
|
||||
Section("Bot Orchestration", "🐙"),
|
||||
Page("pages/master_conf/app.py", "Credentials", "🗝️"),
|
||||
Page("pages/launch_bot/app.py", "Launch Bot", "🙌"),
|
||||
Page("pages/bot_orchestration/app.py", "Instances", "🦅"),
|
||||
Page("pages/file_manager/app.py", "Strategy Configs", "🗂"),
|
||||
Page("pages/file_manager/app.py", "File Explorer", "🗂"),
|
||||
Section("Backtest Manager", "⚙️"),
|
||||
Page("pages/candles_downloader/app.py", "Get Data", "💾"),
|
||||
Page("pages/backtest_manager/create.py", "Create", "⚔️"),
|
||||
Page("pages/backtest_manager/optimize.py", "Optimize", "🧪"),
|
||||
Page("pages/backtest_manager/analyze.py", "Analyze", "🔬"),
|
||||
Page("pages/backtest_manager/analyze_v2.py", "Analyze v2", "🔬"),
|
||||
Page("pages/backtest_manager/simulate.py", "Simulate", "📈"),
|
||||
# Page("pages/backtest_manager/simulate.py", "Simulate", "📈"),
|
||||
Page("pages/launch_bot/app.py", "Deploy", "🙌"),
|
||||
Section("Community Pages", "👨👩👧👦"),
|
||||
Page("pages/strategy_performance/app.py", "Strategy Performance", "🚀"),
|
||||
Page("pages/db_inspector/app.py", "DB Inspector", "🔍"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from hummingbot.core.data_type.common import PositionMode, TradeType, OrderType
|
||||
from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel, TripleBarrierConf
|
||||
from hummingbot.smart_components.strategy_frameworks.directional_trading import DirectionalTradingBacktestingEngine
|
||||
from hummingbot.smart_components.utils import ConfigEncoderDecoder
|
||||
|
||||
@@ -8,13 +8,15 @@ import constants
|
||||
import os
|
||||
import json
|
||||
import streamlit as st
|
||||
from decimal import Decimal
|
||||
|
||||
from quants_lab.strategy.strategy_analysis import StrategyAnalysis
|
||||
from utils.graphs import BacktestingGraphs
|
||||
from utils.optuna_database_manager import OptunaDBManager
|
||||
from utils.os_utils import load_controllers, dump_dict_to_yaml
|
||||
from utils.os_utils import load_controllers
|
||||
from utils.st_utils import initialize_st_page
|
||||
|
||||
|
||||
initialize_st_page(title="Analyze", icon="🔬", initial_sidebar_state="collapsed")
|
||||
|
||||
|
||||
@@ -50,9 +52,23 @@ else:
|
||||
study_selected = st.selectbox("Select a study:", studies.keys())
|
||||
# Filter trials from selected study
|
||||
merged_df = opt_db.merged_df[opt_db.merged_df["study_name"] == study_selected]
|
||||
bt_graphs = BacktestingGraphs(merged_df)
|
||||
# Show and compare all of the study trials
|
||||
st.plotly_chart(bt_graphs.pnl_vs_maxdrawdown(), use_container_width=True)
|
||||
filters_column, scatter_column = st.columns([1, 6])
|
||||
with filters_column:
|
||||
accuracy = st.slider("Accuracy", min_value=0.0, max_value=1.0, value=[0.4, 1.0], step=0.01)
|
||||
net_profit = st.slider("Net PNL (%)", min_value=merged_df["net_pnl_pct"].min(), max_value=merged_df["net_pnl_pct"].max(),
|
||||
value=[merged_df["net_pnl_pct"].min(), merged_df["net_pnl_pct"].max()], step=0.01)
|
||||
max_drawdown = st.slider("Max Drawdown (%)", min_value=merged_df["max_drawdown_pct"].min(), max_value=merged_df["max_drawdown_pct"].max(),
|
||||
value=[merged_df["max_drawdown_pct"].min(), merged_df["max_drawdown_pct"].max()], step=0.01)
|
||||
total_positions = st.slider("Total Positions", min_value=merged_df["total_positions"].min(), max_value=merged_df["total_positions"].max(),
|
||||
value=[merged_df["total_positions"].min(), merged_df["total_positions"].max()], step=1)
|
||||
net_profit_filter = (merged_df["net_pnl_pct"] >= net_profit[0]) & (merged_df["net_pnl_pct"] <= net_profit[1])
|
||||
accuracy_filter = (merged_df["accuracy"] >= accuracy[0]) & (merged_df["accuracy"] <= accuracy[1])
|
||||
max_drawdown_filter = (merged_df["max_drawdown_pct"] >= max_drawdown[0]) & (merged_df["max_drawdown_pct"] <= max_drawdown[1])
|
||||
total_positions_filter = (merged_df["total_positions"] >= total_positions[0]) & (merged_df["total_positions"] <= total_positions[1])
|
||||
with scatter_column:
|
||||
bt_graphs = BacktestingGraphs(merged_df[net_profit_filter & accuracy_filter & max_drawdown_filter & total_positions_filter])
|
||||
# Show and compare all of the study trials
|
||||
st.plotly_chart(bt_graphs.pnl_vs_maxdrawdown(), use_container_width=True)
|
||||
# Get study trials
|
||||
trials = studies[study_selected]
|
||||
# Choose trial
|
||||
@@ -71,59 +87,105 @@ else:
|
||||
# Get field schema
|
||||
field_schema = controller["config"].schema()["properties"]
|
||||
|
||||
c1, c2 = st.columns([5, 1])
|
||||
# Render every field according to schema
|
||||
with c1:
|
||||
columns = st.columns(4)
|
||||
column_index = 0
|
||||
for field_name, properties in field_schema.items():
|
||||
field_type = properties.get("type", "string")
|
||||
field_value = trial_config[field_name]
|
||||
columns = st.columns(4)
|
||||
column_index = 0
|
||||
for field_name, properties in field_schema.items():
|
||||
field_type = properties.get("type", "string")
|
||||
field_value = trial_config[field_name]
|
||||
if field_name not in ["candles_config", "order_levels", "position_mode"]:
|
||||
with columns[column_index]:
|
||||
if field_type == "array" or field_name == "position_mode":
|
||||
pass
|
||||
elif field_type in ["number", "integer"]:
|
||||
if field_type in ["number", "integer"]:
|
||||
field_value = st.number_input(field_name,
|
||||
value=field_value,
|
||||
min_value=properties.get("minimum"),
|
||||
# max_value=properties.get("maximum"),
|
||||
max_value=properties.get("maximum"),
|
||||
key=field_name)
|
||||
elif field_type in ["string"]:
|
||||
elif field_type == "string":
|
||||
field_value = st.text_input(field_name, value=field_value)
|
||||
elif field_type == "boolean":
|
||||
# TODO: Add support for boolean fields in optimize tab
|
||||
field_value = st.checkbox(field_name, value=field_value)
|
||||
else:
|
||||
raise ValueError(f"Field type {field_type} not supported")
|
||||
try:
|
||||
# TODO: figure out how to make this configurable
|
||||
if field_name == "candles_config":
|
||||
candles_config = [CandlesConfig(**value) for value in field_value]
|
||||
st.session_state["strategy_params"][field_name] = candles_config
|
||||
elif field_name == "order_levels":
|
||||
order_levels = [OrderLevel(**value) for value in field_value]
|
||||
st.session_state["strategy_params"][field_name] = order_levels
|
||||
st.session_state["strategy_params"][field_name] = field_value
|
||||
except KeyError as e:
|
||||
pass
|
||||
column_index = (column_index + 1) % 4
|
||||
with c2:
|
||||
add_positions = st.checkbox("Add positions", value=True)
|
||||
add_volume = st.checkbox("Add volume", value=True)
|
||||
add_pnl = st.checkbox("Add PnL", value=True)
|
||||
else:
|
||||
if field_name == "candles_config":
|
||||
st.write("---")
|
||||
st.write(f"## Candles Config:")
|
||||
candles = []
|
||||
for i, candles_config in enumerate(field_value):
|
||||
st.write(f"#### Candle {i}:")
|
||||
c11, c12, c13, c14 = st.columns(4)
|
||||
with c11:
|
||||
connector = st.text_input("Connector", value=candles_config["connector"])
|
||||
with c12:
|
||||
trading_pair = st.text_input("Trading pair", value=candles_config["trading_pair"])
|
||||
with c13:
|
||||
interval = st.text_input("Interval", value=candles_config["interval"])
|
||||
with c14:
|
||||
max_records = st.number_input("Max records", value=candles_config["max_records"])
|
||||
st.write("---")
|
||||
candles.append(CandlesConfig(connector=connector, trading_pair=trading_pair, interval=interval,
|
||||
max_records=max_records))
|
||||
field_value = candles
|
||||
elif field_name == "order_levels":
|
||||
new_levels = []
|
||||
st.write(f"## Order Levels:")
|
||||
for order_level in field_value:
|
||||
st.write(f"### Level {order_level['level']} {order_level['side'].name}")
|
||||
ol_c1, ol_c2 = st.columns([5, 1])
|
||||
with ol_c1:
|
||||
st.write("#### Triple Barrier config:")
|
||||
c21, c22, c23, c24, c25 = st.columns(5)
|
||||
triple_barrier_conf_level = order_level["triple_barrier_conf"]
|
||||
with c21:
|
||||
take_profit = st.number_input("Take profit", value=float(triple_barrier_conf_level["take_profit"]),
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_tp")
|
||||
with c22:
|
||||
stop_loss = st.number_input("Stop Loss", value=float(triple_barrier_conf_level["stop_loss"]),
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_sl")
|
||||
with c23:
|
||||
time_limit = st.number_input("Time Limit", value=triple_barrier_conf_level["time_limit"],
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_tl")
|
||||
with c24:
|
||||
ts_ap = st.number_input("Trailing Stop Activation Price", value=float(triple_barrier_conf_level["trailing_stop_activation_price_delta"]),
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_tsap", format="%.4f")
|
||||
with c25:
|
||||
ts_td = st.number_input("Trailing Stop Trailing Delta", value=float(triple_barrier_conf_level["trailing_stop_trailing_delta"]),
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_tstd", format="%.4f")
|
||||
with ol_c2:
|
||||
st.write("#### Position config:")
|
||||
c31, c32 = st.columns(2)
|
||||
with c31:
|
||||
order_amount = st.number_input("Order amount USD", value=float(order_level["order_amount_usd"]),
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_oa")
|
||||
with c32:
|
||||
cooldown_time = st.number_input("Cooldown time", value=order_level["cooldown_time"],
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_cd")
|
||||
triple_barrier_conf = TripleBarrierConf(stop_loss=Decimal(stop_loss), take_profit=Decimal(take_profit),
|
||||
time_limit=time_limit,
|
||||
trailing_stop_activation_price_delta=Decimal(ts_ap),
|
||||
trailing_stop_trailing_delta=Decimal(ts_td),
|
||||
open_order_type=OrderType.MARKET)
|
||||
new_levels.append(OrderLevel(level=order_level["level"], side=order_level["side"],
|
||||
order_amount_usd=order_amount, cooldown_time=cooldown_time,
|
||||
triple_barrier_conf=triple_barrier_conf))
|
||||
st.write("---")
|
||||
|
||||
# Backtesting parameters section
|
||||
st.write("## Backtesting parameters")
|
||||
# # Get every trial params
|
||||
# # TODO: Filter only from selected study
|
||||
backtesting_configs = opt_db.load_params()
|
||||
# # Get trial backtesting params
|
||||
backtesting_params = backtesting_configs[trial_selected]
|
||||
field_value = new_levels
|
||||
elif field_name == "position_mode":
|
||||
field_value = PositionMode.HEDGE
|
||||
else:
|
||||
field_value = None
|
||||
st.session_state["strategy_params"][field_name] = field_value
|
||||
|
||||
column_index = (column_index + 1) % 4
|
||||
|
||||
st.write("### Backtesting period")
|
||||
col1, col2, col3, col4 = st.columns([1, 1, 1, 0.5])
|
||||
with col1:
|
||||
trade_cost = st.number_input("Trade cost",
|
||||
value=0.0006,
|
||||
min_value=0.0001, format="%.4f",)
|
||||
min_value=0.0001, format="%.4f", )
|
||||
with col2:
|
||||
initial_portfolio_usd = st.number_input("Initial portfolio usd",
|
||||
value=10000.00,
|
||||
@@ -134,18 +196,16 @@ else:
|
||||
end = st.text_input("End", value="2023-08-01")
|
||||
c1, c2 = st.columns([1, 1])
|
||||
with col4:
|
||||
deploy_button = st.button("💾Save controller config!")
|
||||
add_positions = st.checkbox("Add positions", value=True)
|
||||
add_volume = st.checkbox("Add volume", value=True)
|
||||
add_pnl = st.checkbox("Add PnL", value=True)
|
||||
save_config = st.button("💾Save controller config!")
|
||||
config = controller["config"](**st.session_state["strategy_params"])
|
||||
controller = controller["class"](config=config)
|
||||
if deploy_button:
|
||||
if save_config:
|
||||
encoder_decoder = ConfigEncoderDecoder(TradeType, OrderType, PositionMode)
|
||||
encoder_decoder.yaml_dump(config.dict(),
|
||||
f"hummingbot_files/controller_configs/{config.strategy_name}_{trial_selected}.yml")
|
||||
# DockerManager().create_hummingbot_instance(instance_name=config.strategy_name,
|
||||
# base_conf_folder=f"{constants.HUMMINGBOT_TEMPLATES}/master_bot_conf/.",
|
||||
# target_conf_folder=f"{constants.BOTS_FOLDER}/{config.strategy_name}/.",
|
||||
# controllers_folder="quants_lab/controllers",
|
||||
# controllers_config_folder="hummingbot_files/controller_configs",
|
||||
# image="dardonacci/hummingbot")
|
||||
run_backtesting_button = st.button("⚙️Run Backtesting!")
|
||||
if run_backtesting_button:
|
||||
try:
|
||||
@@ -158,9 +218,10 @@ else:
|
||||
positions=backtesting_results["executors_df"],
|
||||
candles_df=backtesting_results["processed_data"],
|
||||
)
|
||||
metrics_container = bt_graphs.get_trial_metrics(strategy_analysis,
|
||||
add_positions=add_positions,
|
||||
add_volume=add_volume)
|
||||
metrics_container = BacktestingGraphs(backtesting_results["processed_data"]).get_trial_metrics(
|
||||
strategy_analysis,
|
||||
add_positions=add_positions,
|
||||
add_volume=add_volume)
|
||||
|
||||
except FileNotFoundError:
|
||||
st.warning(f"The requested candles could not be found.")
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
from hummingbot.core.data_type.common import PositionMode, TradeType, OrderType
|
||||
from hummingbot.data_feed.candles_feed.candles_factory import CandlesConfig
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel, TripleBarrierConf
|
||||
from hummingbot.smart_components.strategy_frameworks.directional_trading import DirectionalTradingBacktestingEngine
|
||||
from hummingbot.smart_components.utils import ConfigEncoderDecoder
|
||||
|
||||
import constants
|
||||
import os
|
||||
import json
|
||||
import streamlit as st
|
||||
from decimal import Decimal
|
||||
|
||||
from quants_lab.strategy.strategy_analysis import StrategyAnalysis
|
||||
from utils.graphs import BacktestingGraphs
|
||||
from utils.optuna_database_manager import OptunaDBManager
|
||||
from utils.os_utils import load_controllers
|
||||
from utils.st_utils import initialize_st_page
|
||||
|
||||
|
||||
initialize_st_page(title="Analyze", icon="🔬", initial_sidebar_state="collapsed")
|
||||
|
||||
|
||||
@st.cache_resource
|
||||
def get_databases():
|
||||
sqlite_files = [db_name for db_name in os.listdir("data/backtesting") if db_name.endswith(".db")]
|
||||
databases_list = [OptunaDBManager(db) for db in sqlite_files]
|
||||
databases_dict = {database.db_name: database for database in databases_list}
|
||||
return [x.db_name for x in databases_dict.values() if x.status == 'OK']
|
||||
|
||||
|
||||
def initialize_session_state_vars():
|
||||
if "strategy_params" not in st.session_state:
|
||||
st.session_state.strategy_params = {}
|
||||
if "backtesting_params" not in st.session_state:
|
||||
st.session_state.backtesting_params = {}
|
||||
|
||||
|
||||
initialize_session_state_vars()
|
||||
dbs = get_databases()
|
||||
if not dbs:
|
||||
st.warning("We couldn't find any Optuna database.")
|
||||
selected_db_name = None
|
||||
selected_db = None
|
||||
else:
|
||||
# Select database from selectbox
|
||||
selected_db = st.selectbox("Select your database:", dbs)
|
||||
# Instantiate database manager
|
||||
opt_db = OptunaDBManager(selected_db)
|
||||
# Load studies
|
||||
studies = opt_db.load_studies()
|
||||
# Choose study
|
||||
study_selected = st.selectbox("Select a study:", studies.keys())
|
||||
# Filter trials from selected study
|
||||
merged_df = opt_db.merged_df[opt_db.merged_df["study_name"] == study_selected]
|
||||
filters_column, scatter_column = st.columns([1, 6])
|
||||
with filters_column:
|
||||
accuracy = st.slider("Accuracy", min_value=0.0, max_value=1.0, value=[0.4, 1.0], step=0.01)
|
||||
net_profit = st.slider("Net PNL (%)", min_value=merged_df["net_pnl_pct"].min(), max_value=merged_df["net_pnl_pct"].max(),
|
||||
value=[merged_df["net_pnl_pct"].min(), merged_df["net_pnl_pct"].max()], step=0.01)
|
||||
max_drawdown = st.slider("Max Drawdown (%)", min_value=merged_df["max_drawdown_pct"].min(), max_value=merged_df["max_drawdown_pct"].max(),
|
||||
value=[merged_df["max_drawdown_pct"].min(), merged_df["max_drawdown_pct"].max()], step=0.01)
|
||||
total_positions = st.slider("Total Positions", min_value=merged_df["total_positions"].min(), max_value=merged_df["total_positions"].max(),
|
||||
value=[merged_df["total_positions"].min(), merged_df["total_positions"].max()], step=1)
|
||||
net_profit_filter = (merged_df["net_pnl_pct"] >= net_profit[0]) & (merged_df["net_pnl_pct"] <= net_profit[1])
|
||||
accuracy_filter = (merged_df["accuracy"] >= accuracy[0]) & (merged_df["accuracy"] <= accuracy[1])
|
||||
max_drawdown_filter = (merged_df["max_drawdown_pct"] >= max_drawdown[0]) & (merged_df["max_drawdown_pct"] <= max_drawdown[1])
|
||||
total_positions_filter = (merged_df["total_positions"] >= total_positions[0]) & (merged_df["total_positions"] <= total_positions[1])
|
||||
with scatter_column:
|
||||
bt_graphs = BacktestingGraphs(merged_df[net_profit_filter & accuracy_filter & max_drawdown_filter & total_positions_filter])
|
||||
# Show and compare all of the study trials
|
||||
st.plotly_chart(bt_graphs.pnl_vs_maxdrawdown(), use_container_width=True)
|
||||
# Get study trials
|
||||
trials = studies[study_selected]
|
||||
# Choose trial
|
||||
trial_selected = st.selectbox("Select a trial to backtest", list(trials.keys()))
|
||||
trial = trials[trial_selected]
|
||||
# Transform trial config in a dictionary
|
||||
encoder_decoder = ConfigEncoderDecoder(TradeType, OrderType, PositionMode)
|
||||
trial_config = encoder_decoder.decode(json.loads(trial["config"]))
|
||||
|
||||
# Strategy parameters section
|
||||
st.write("## Strategy parameters")
|
||||
# Load strategies (class, config, module)
|
||||
controllers = load_controllers(constants.CONTROLLERS_PATH)
|
||||
# Select strategy
|
||||
controller = controllers[trial_config["strategy_name"]]
|
||||
# Get field schema
|
||||
field_schema = controller["config"].schema()["properties"]
|
||||
|
||||
columns = st.columns(4)
|
||||
column_index = 0
|
||||
for field_name, properties in field_schema.items():
|
||||
field_type = properties.get("type", "string")
|
||||
field_value = trial_config[field_name]
|
||||
if field_name not in ["candles_config", "order_levels", "position_mode"]:
|
||||
with columns[column_index]:
|
||||
if field_type in ["number", "integer"]:
|
||||
field_value = st.number_input(field_name,
|
||||
value=field_value,
|
||||
min_value=properties.get("minimum"),
|
||||
max_value=properties.get("maximum"),
|
||||
key=field_name)
|
||||
elif field_type == "string":
|
||||
field_value = st.text_input(field_name, value=field_value)
|
||||
elif field_type == "boolean":
|
||||
# TODO: Add support for boolean fields in optimize tab
|
||||
field_value = st.checkbox(field_name, value=field_value)
|
||||
else:
|
||||
raise ValueError(f"Field type {field_type} not supported")
|
||||
else:
|
||||
if field_name == "candles_config":
|
||||
st.write("---")
|
||||
st.write(f"## Candles Config:")
|
||||
candles = []
|
||||
for i, candles_config in enumerate(field_value):
|
||||
st.write(f"#### Candle {i}:")
|
||||
c11, c12, c13, c14 = st.columns(4)
|
||||
with c11:
|
||||
connector = st.text_input("Connector", value=candles_config["connector"])
|
||||
with c12:
|
||||
trading_pair = st.text_input("Trading pair", value=candles_config["trading_pair"])
|
||||
with c13:
|
||||
interval = st.text_input("Interval", value=candles_config["interval"])
|
||||
with c14:
|
||||
max_records = st.number_input("Max records", value=candles_config["max_records"])
|
||||
st.write("---")
|
||||
candles.append(CandlesConfig(connector=connector, trading_pair=trading_pair, interval=interval,
|
||||
max_records=max_records))
|
||||
field_value = candles
|
||||
elif field_name == "order_levels":
|
||||
new_levels = []
|
||||
st.write(f"## Order Levels:")
|
||||
for order_level in field_value:
|
||||
st.write(f"### Level {order_level['level']} {order_level['side'].name}")
|
||||
ol_c1, ol_c2 = st.columns([5, 1])
|
||||
with ol_c1:
|
||||
st.write("#### Triple Barrier config:")
|
||||
c21, c22, c23, c24, c25 = st.columns(5)
|
||||
triple_barrier_conf_level = order_level["triple_barrier_conf"]
|
||||
with c21:
|
||||
take_profit = st.number_input("Take profit", value=float(triple_barrier_conf_level["take_profit"]),
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_tp")
|
||||
with c22:
|
||||
stop_loss = st.number_input("Stop Loss", value=float(triple_barrier_conf_level["stop_loss"]),
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_sl")
|
||||
with c23:
|
||||
time_limit = st.number_input("Time Limit", value=triple_barrier_conf_level["time_limit"],
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_tl")
|
||||
with c24:
|
||||
ts_ap = st.number_input("Trailing Stop Activation Price", value=float(triple_barrier_conf_level["trailing_stop_activation_price_delta"]),
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_tsap", format="%.4f")
|
||||
with c25:
|
||||
ts_td = st.number_input("Trailing Stop Trailing Delta", value=float(triple_barrier_conf_level["trailing_stop_trailing_delta"]),
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_tstd", format="%.4f")
|
||||
with ol_c2:
|
||||
st.write("#### Position config:")
|
||||
c31, c32 = st.columns(2)
|
||||
with c31:
|
||||
order_amount = st.number_input("Order amount USD", value=float(order_level["order_amount_usd"]),
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_oa")
|
||||
with c32:
|
||||
cooldown_time = st.number_input("Cooldown time", value=order_level["cooldown_time"],
|
||||
key=f"{order_level['level']}_{order_level['side'].name}_cd")
|
||||
triple_barrier_conf = TripleBarrierConf(stop_loss=Decimal(stop_loss), take_profit=Decimal(take_profit),
|
||||
time_limit=time_limit,
|
||||
trailing_stop_activation_price_delta=Decimal(ts_ap),
|
||||
trailing_stop_trailing_delta=Decimal(ts_td),
|
||||
open_order_type=OrderType.MARKET)
|
||||
new_levels.append(OrderLevel(level=order_level["level"], side=order_level["side"],
|
||||
order_amount_usd=order_amount, cooldown_time=cooldown_time,
|
||||
triple_barrier_conf=triple_barrier_conf))
|
||||
st.write("---")
|
||||
|
||||
field_value = new_levels
|
||||
elif field_name == "position_mode":
|
||||
field_value = PositionMode.HEDGE
|
||||
else:
|
||||
field_value = None
|
||||
st.session_state["strategy_params"][field_name] = field_value
|
||||
|
||||
column_index = (column_index + 1) % 4
|
||||
|
||||
st.write("### Backtesting period")
|
||||
col1, col2, col3, col4 = st.columns([1, 1, 1, 0.5])
|
||||
with col1:
|
||||
trade_cost = st.number_input("Trade cost",
|
||||
value=0.0006,
|
||||
min_value=0.0001, format="%.4f", )
|
||||
with col2:
|
||||
initial_portfolio_usd = st.number_input("Initial portfolio usd",
|
||||
value=10000.00,
|
||||
min_value=1.00,
|
||||
max_value=999999999.99)
|
||||
with col3:
|
||||
start = st.text_input("Start", value="2023-01-01")
|
||||
end = st.text_input("End", value="2023-08-01")
|
||||
c1, c2 = st.columns([1, 1])
|
||||
with col4:
|
||||
add_positions = st.checkbox("Add positions", value=True)
|
||||
add_volume = st.checkbox("Add volume", value=True)
|
||||
add_pnl = st.checkbox("Add PnL", value=True)
|
||||
save_config = st.button("💾Save controller config!")
|
||||
config = controller["config"](**st.session_state["strategy_params"])
|
||||
controller = controller["class"](config=config)
|
||||
if save_config:
|
||||
encoder_decoder = ConfigEncoderDecoder(TradeType, OrderType, PositionMode)
|
||||
encoder_decoder.yaml_dump(config.dict(),
|
||||
f"hummingbot_files/controller_configs/{config.strategy_name}_{trial_selected}.yml")
|
||||
run_backtesting_button = st.button("⚙️Run Backtesting!")
|
||||
if run_backtesting_button:
|
||||
try:
|
||||
engine = DirectionalTradingBacktestingEngine(controller=controller)
|
||||
engine.load_controller_data("./data/candles")
|
||||
backtesting_results = engine.run_backtesting(initial_portfolio_usd=initial_portfolio_usd,
|
||||
trade_cost=trade_cost,
|
||||
start=start, end=end)
|
||||
strategy_analysis = StrategyAnalysis(
|
||||
positions=backtesting_results["executors_df"],
|
||||
candles_df=backtesting_results["processed_data"],
|
||||
)
|
||||
metrics_container = BacktestingGraphs(backtesting_results["processed_data"]).get_trial_metrics(
|
||||
strategy_analysis,
|
||||
add_positions=add_positions,
|
||||
add_volume=add_volume)
|
||||
|
||||
except FileNotFoundError:
|
||||
st.warning(f"The requested candles could not be found.")
|
||||
@@ -20,7 +20,7 @@ with c1:
|
||||
exchange = st.selectbox("Exchange", ["binance_perpetual", "binance"], index=0)
|
||||
trading_pairs = st.text_input("Trading Pairs (separated with commas)", value="BTC-USDT,ETH-USDT")
|
||||
with c2:
|
||||
intervals = st.multiselect("Intervals", options=["1m", "3m", "5m", "15m", "1h", "4h", "1d"], default=["1m", "3m", "1h"])
|
||||
intervals = st.multiselect("Intervals", options=["1s", "1m", "3m", "5m", "15m", "1h", "4h", "1d"], default=["1m", "3m", "1h"])
|
||||
days_to_download = st.number_input("Days to Download", value=30, min_value=1, max_value=365, step=1)
|
||||
with c3:
|
||||
get_data_button = st.button("Download Candles!")
|
||||
|
||||
@@ -2,6 +2,7 @@ import glob
|
||||
import os
|
||||
from types import SimpleNamespace
|
||||
import streamlit as st
|
||||
from docker_manager import DockerManager
|
||||
from streamlit_elements import elements, mui
|
||||
|
||||
import constants
|
||||
@@ -14,6 +15,10 @@ from utils.st_utils import initialize_st_page
|
||||
|
||||
initialize_st_page(title="Credentials", icon="🗝️", initial_sidebar_state="collapsed")
|
||||
|
||||
docker_manager = DockerManager()
|
||||
if not docker_manager.is_docker_running():
|
||||
st.warning("Docker is not running. Please start Docker and refresh the page.")
|
||||
st.stop()
|
||||
|
||||
if "mc_board" not in st.session_state:
|
||||
board = Dashboard()
|
||||
|
||||
100
quants_lab/controllers/dman_v1.py
Normal file
100
quants_lab/controllers/dman_v1.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import time
|
||||
from decimal import Decimal
|
||||
|
||||
import pandas_ta as ta # noqa: F401
|
||||
|
||||
from hummingbot.core.data_type.common import TradeType
|
||||
from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig, TrailingStop
|
||||
from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
|
||||
from hummingbot.smart_components.strategy_frameworks.market_making.market_making_controller_base import (
|
||||
MarketMakingControllerBase,
|
||||
MarketMakingControllerConfigBase,
|
||||
)
|
||||
|
||||
|
||||
class DManV1Config(MarketMakingControllerConfigBase):
|
||||
strategy_name: str = "dman_v1"
|
||||
natr_length: int = 14
|
||||
|
||||
|
||||
class DManV1(MarketMakingControllerBase):
|
||||
"""
|
||||
Directional Market Making Strategy making use of NATR indicator to make spreads dynamic.
|
||||
"""
|
||||
|
||||
def __init__(self, config: DManV1Config):
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
|
||||
def refresh_order_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
|
||||
"""
|
||||
Checks if the order needs to be refreshed.
|
||||
You can reimplement this method to add more conditions.
|
||||
"""
|
||||
if executor.position_config.timestamp + order_level.order_refresh_time > time.time():
|
||||
return False
|
||||
return True
|
||||
|
||||
def early_stop_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
|
||||
"""
|
||||
If an executor has an active position, should we close it based on a condition.
|
||||
"""
|
||||
return False
|
||||
|
||||
def cooldown_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
|
||||
"""
|
||||
After finishing an order, the executor will be in cooldown for a certain amount of time.
|
||||
This prevents the executor from creating a new order immediately after finishing one and execute a lot
|
||||
of orders in a short period of time from the same side.
|
||||
"""
|
||||
if executor.close_timestamp and executor.close_timestamp + order_level.cooldown_time > time.time():
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_processed_data(self):
|
||||
"""
|
||||
Gets the price and spread multiplier from the last candlestick.
|
||||
"""
|
||||
candles_df = self.candles[0].candles_df
|
||||
natr = ta.natr(candles_df["high"], candles_df["low"], candles_df["close"], length=self.config.natr_length) / 100
|
||||
|
||||
candles_df["spread_multiplier"] = natr
|
||||
candles_df["price_multiplier"] = 0.0
|
||||
return candles_df
|
||||
|
||||
def get_position_config(self, order_level: OrderLevel) -> PositionConfig:
|
||||
"""
|
||||
Creates a PositionConfig object from an OrderLevel object.
|
||||
Here you can use technical indicators to determine the parameters of the position config.
|
||||
"""
|
||||
close_price = self.get_close_price(self.config.exchange, self.config.trading_pair)
|
||||
amount = order_level.order_amount_usd / close_price
|
||||
price_multiplier, spread_multiplier, side_filter = self.get_price_and_spread_multiplier()
|
||||
|
||||
price_adjusted = close_price * (1 + price_multiplier)
|
||||
side_multiplier = -1 if order_level.side == TradeType.BUY else 1
|
||||
order_price = price_adjusted * (1 + order_level.spread_factor * spread_multiplier * side_multiplier)
|
||||
if order_level.triple_barrier_conf.trailing_stop_trailing_delta and order_level.triple_barrier_conf.trailing_stop_trailing_delta:
|
||||
trailing_stop = TrailingStop(
|
||||
activation_price_delta=order_level.triple_barrier_conf.trailing_stop_activation_price_delta,
|
||||
trailing_delta=order_level.triple_barrier_conf.trailing_stop_trailing_delta,
|
||||
)
|
||||
else:
|
||||
trailing_stop = None
|
||||
position_config = PositionConfig(
|
||||
timestamp=time.time(),
|
||||
trading_pair=self.config.trading_pair,
|
||||
exchange=self.config.exchange,
|
||||
side=order_level.side,
|
||||
amount=amount,
|
||||
take_profit=order_level.triple_barrier_conf.take_profit,
|
||||
stop_loss=order_level.triple_barrier_conf.stop_loss,
|
||||
time_limit=order_level.triple_barrier_conf.time_limit,
|
||||
entry_price=Decimal(order_price),
|
||||
open_order_type=order_level.triple_barrier_conf.open_order_type,
|
||||
take_profit_order_type=order_level.triple_barrier_conf.take_profit_order_type,
|
||||
trailing_stop=trailing_stop,
|
||||
leverage=self.config.leverage
|
||||
)
|
||||
return position_config
|
||||
112
quants_lab/controllers/dman_v2.py
Normal file
112
quants_lab/controllers/dman_v2.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import time
|
||||
from decimal import Decimal
|
||||
|
||||
import pandas_ta as ta # noqa: F401
|
||||
|
||||
from hummingbot.core.data_type.common import TradeType
|
||||
from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig, TrailingStop
|
||||
from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
|
||||
from hummingbot.smart_components.strategy_frameworks.market_making.market_making_controller_base import (
|
||||
MarketMakingControllerBase,
|
||||
MarketMakingControllerConfigBase,
|
||||
)
|
||||
|
||||
|
||||
class DManV2Config(MarketMakingControllerConfigBase):
|
||||
strategy_name: str = "dman_v2"
|
||||
macd_fast: int = 12
|
||||
macd_slow: int = 26
|
||||
macd_signal: int = 9
|
||||
natr_length: int = 14
|
||||
|
||||
|
||||
class DManV2(MarketMakingControllerBase):
|
||||
"""
|
||||
Directional Market Making Strategy making use of NATR indicator to make spreads dynamic and shift the mid price.
|
||||
"""
|
||||
|
||||
def __init__(self, config: DManV2Config):
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
|
||||
def refresh_order_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
|
||||
"""
|
||||
Checks if the order needs to be refreshed.
|
||||
You can reimplement this method to add more conditions.
|
||||
"""
|
||||
if executor.position_config.timestamp + order_level.order_refresh_time > time.time():
|
||||
return False
|
||||
return True
|
||||
|
||||
def early_stop_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
|
||||
"""
|
||||
If an executor has an active position, should we close it based on a condition.
|
||||
"""
|
||||
return False
|
||||
|
||||
def cooldown_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
|
||||
"""
|
||||
After finishing an order, the executor will be in cooldown for a certain amount of time.
|
||||
This prevents the executor from creating a new order immediately after finishing one and execute a lot
|
||||
of orders in a short period of time from the same side.
|
||||
"""
|
||||
if executor.close_timestamp and executor.close_timestamp + order_level.cooldown_time > time.time():
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_processed_data(self):
|
||||
"""
|
||||
Gets the price and spread multiplier from the last candlestick.
|
||||
"""
|
||||
candles_df = self.candles[0].candles_df
|
||||
natr = ta.natr(candles_df["high"], candles_df["low"], candles_df["close"], length=self.config.natr_length) / 100
|
||||
|
||||
macd_output = ta.macd(candles_df["close"], fast=self.config.macd_fast, slow=self.config.macd_slow, signal=self.config.macd_signal)
|
||||
macd = macd_output[f"MACD_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"]
|
||||
macdh = macd_output[f"MACDh_{self.config.macd_fast}_{self.config.macd_slow}_{self.config.macd_signal}"]
|
||||
macd_signal = - (macd - macd.mean()) / macd.std()
|
||||
macdh_signal = macdh.apply(lambda x: 1 if x > 0 else -1)
|
||||
max_price_shift = natr / 2
|
||||
|
||||
price_multiplier = (0.5 * macd_signal + 0.5 * macdh_signal) * max_price_shift
|
||||
|
||||
candles_df["spread_multiplier"] = natr
|
||||
candles_df["price_multiplier"] = price_multiplier
|
||||
return candles_df
|
||||
|
||||
def get_position_config(self, order_level: OrderLevel) -> PositionConfig:
|
||||
"""
|
||||
Creates a PositionConfig object from an OrderLevel object.
|
||||
Here you can use technical indicators to determine the parameters of the position config.
|
||||
"""
|
||||
close_price = self.get_close_price(self.config.exchange, self.config.trading_pair)
|
||||
amount = order_level.order_amount_usd / close_price
|
||||
price_multiplier, spread_multiplier, side_filter = self.get_price_and_spread_multiplier()
|
||||
|
||||
price_adjusted = close_price * (1 + price_multiplier)
|
||||
side_multiplier = -1 if order_level.side == TradeType.BUY else 1
|
||||
order_price = price_adjusted * (1 + order_level.spread_factor * spread_multiplier * side_multiplier)
|
||||
if order_level.triple_barrier_conf.trailing_stop_trailing_delta and order_level.triple_barrier_conf.trailing_stop_trailing_delta:
|
||||
trailing_stop = TrailingStop(
|
||||
activation_price_delta=order_level.triple_barrier_conf.trailing_stop_activation_price_delta,
|
||||
trailing_delta=order_level.triple_barrier_conf.trailing_stop_trailing_delta,
|
||||
)
|
||||
else:
|
||||
trailing_stop = None
|
||||
position_config = PositionConfig(
|
||||
timestamp=time.time(),
|
||||
trading_pair=self.config.trading_pair,
|
||||
exchange=self.config.exchange,
|
||||
side=order_level.side,
|
||||
amount=amount,
|
||||
take_profit=order_level.triple_barrier_conf.take_profit,
|
||||
stop_loss=order_level.triple_barrier_conf.stop_loss,
|
||||
time_limit=order_level.triple_barrier_conf.time_limit,
|
||||
entry_price=Decimal(order_price),
|
||||
open_order_type=order_level.triple_barrier_conf.open_order_type,
|
||||
take_profit_order_type=order_level.triple_barrier_conf.take_profit_order_type,
|
||||
trailing_stop=trailing_stop,
|
||||
leverage=self.config.leverage
|
||||
)
|
||||
return position_config
|
||||
102
quants_lab/controllers/dman_v3.py
Normal file
102
quants_lab/controllers/dman_v3.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import time
|
||||
from decimal import Decimal
|
||||
|
||||
import pandas_ta as ta # noqa: F401
|
||||
|
||||
from hummingbot.core.data_type.common import TradeType
|
||||
from hummingbot.smart_components.executors.position_executor.data_types import PositionConfig, TrailingStop
|
||||
from hummingbot.smart_components.executors.position_executor.position_executor import PositionExecutor
|
||||
from hummingbot.smart_components.strategy_frameworks.data_types import OrderLevel
|
||||
from hummingbot.smart_components.strategy_frameworks.market_making.market_making_controller_base import (
|
||||
MarketMakingControllerBase,
|
||||
MarketMakingControllerConfigBase,
|
||||
)
|
||||
|
||||
|
||||
class DManV3Config(MarketMakingControllerConfigBase):
|
||||
strategy_name: str = "dman_v3"
|
||||
bb_length: int = 100
|
||||
bb_std: float = 2.0
|
||||
|
||||
|
||||
class DManV3(MarketMakingControllerBase):
|
||||
"""
|
||||
Directional Market Making Strategy making use of NATR indicator to make spreads dynamic and shift the mid price.
|
||||
"""
|
||||
|
||||
def __init__(self, config: DManV3Config):
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
|
||||
def refresh_order_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
|
||||
"""
|
||||
Checks if the order needs to be refreshed.
|
||||
You can reimplement this method to add more conditions.
|
||||
"""
|
||||
if executor.position_config.timestamp + order_level.order_refresh_time > time.time():
|
||||
return False
|
||||
return True
|
||||
|
||||
def early_stop_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
|
||||
"""
|
||||
If an executor has an active position, should we close it based on a condition.
|
||||
"""
|
||||
return False
|
||||
|
||||
def cooldown_condition(self, executor: PositionExecutor, order_level: OrderLevel) -> bool:
|
||||
"""
|
||||
After finishing an order, the executor will be in cooldown for a certain amount of time.
|
||||
This prevents the executor from creating a new order immediately after finishing one and execute a lot
|
||||
of orders in a short period of time from the same side.
|
||||
"""
|
||||
if executor.close_timestamp and executor.close_timestamp + order_level.cooldown_time > time.time():
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_processed_data(self):
|
||||
"""
|
||||
Gets the price and spread multiplier from the last candlestick.
|
||||
"""
|
||||
candles_df = self.candles[0].candles_df
|
||||
bbp = ta.bbands(candles_df["close"], length=self.config.bb_length, std=self.config.bb_std)
|
||||
|
||||
candles_df["spread_multiplier"] = bbp[f"BBB_{self.config.bb_length}_{self.config.bb_std}"] / 200
|
||||
candles_df["price_multiplier"] = bbp[f"BBM_{self.config.bb_length}_{self.config.bb_std}"]
|
||||
return candles_df
|
||||
|
||||
def get_position_config(self, order_level: OrderLevel) -> PositionConfig:
|
||||
"""
|
||||
Creates a PositionConfig object from an OrderLevel object.
|
||||
Here you can use technical indicators to determine the parameters of the position config.
|
||||
"""
|
||||
close_price = self.get_close_price(self.config.exchange, self.config.trading_pair)
|
||||
|
||||
amount = order_level.order_amount_usd / close_price
|
||||
price_multiplier, spread_multiplier = self.get_price_and_spread_multiplier()
|
||||
side_multiplier = -1 if order_level.side == TradeType.BUY else 1
|
||||
|
||||
order_spread_multiplier = order_level.spread_factor * spread_multiplier * side_multiplier
|
||||
order_price = price_multiplier * (1 + order_spread_multiplier)
|
||||
if order_level.triple_barrier_conf.trailing_stop_trailing_delta and order_level.triple_barrier_conf.trailing_stop_trailing_delta:
|
||||
trailing_stop = TrailingStop(
|
||||
activation_price_delta=order_level.triple_barrier_conf.trailing_stop_activation_price_delta,
|
||||
trailing_delta=order_level.triple_barrier_conf.trailing_stop_trailing_delta,
|
||||
)
|
||||
else:
|
||||
trailing_stop = None
|
||||
position_config = PositionConfig(
|
||||
timestamp=time.time(),
|
||||
trading_pair=self.config.trading_pair,
|
||||
exchange=self.config.exchange,
|
||||
side=order_level.side,
|
||||
amount=amount,
|
||||
take_profit=order_level.triple_barrier_conf.take_profit,
|
||||
stop_loss=order_level.triple_barrier_conf.stop_loss,
|
||||
time_limit=order_level.triple_barrier_conf.time_limit,
|
||||
entry_price=Decimal(order_price),
|
||||
open_order_type=order_level.triple_barrier_conf.open_order_type,
|
||||
take_profit_order_type=order_level.triple_barrier_conf.take_profit_order_type,
|
||||
trailing_stop=trailing_stop,
|
||||
leverage=self.config.leverage
|
||||
)
|
||||
return position_config
|
||||
@@ -44,7 +44,7 @@ class BotPerformanceCard(Dashboard.Item):
|
||||
|
||||
def __call__(self, bot_config: dict):
|
||||
bot_name = bot_config["bot_name"]
|
||||
scripts_directory = f"./hummingbot_files/bot_configs/{bot_config['bot_name']}"
|
||||
scripts_directory = f"./hummingbot_files/bots/{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)]
|
||||
|
||||
@@ -33,6 +33,8 @@ class LaunchBotCard(Dashboard.Item):
|
||||
DockerManager().create_hummingbot_instance(instance_name=bot_name,
|
||||
base_conf_folder=f"{constants.HUMMINGBOT_TEMPLATES}/{self._base_bot_config}/.",
|
||||
target_conf_folder=f"{constants.BOTS_FOLDER}/{bot_name}/.",
|
||||
controllers_folder=constants.CONTROLLERS_PATH,
|
||||
controllers_config_folder=constants.CONTROLLERS_CONFIG_PATH,
|
||||
image=self._image_name)
|
||||
with st.spinner('Starting Master Configs instance... This process may take a few seconds'):
|
||||
time.sleep(3)
|
||||
|
||||
@@ -28,7 +28,7 @@ class OptimizationCreationCard(Dashboard.Item):
|
||||
|
||||
def __call__(self):
|
||||
available_strategies = load_controllers(constants.CONTROLLERS_PATH)
|
||||
strategy_names = list(available_strategies.keys())
|
||||
strategy_names = [strategy for strategy, strategy_info in available_strategies.items() if "class" in strategy_info]
|
||||
with mui.Paper(key=self._key,
|
||||
sx={"display": "flex", "flexDirection": "column", "borderRadius": 3, "overflow": "hidden"},
|
||||
elevation=1):
|
||||
|
||||
@@ -122,7 +122,7 @@ def objective(trial):
|
||||
# amounts for going long or short, the cooldown time between orders and the triple barrier configuration
|
||||
stop_loss = trial.suggest_float('stop_loss', 0.01, 0.02, step=0.01)
|
||||
take_profit = trial.suggest_float('take_profit', 0.01, 0.05, step=0.01)
|
||||
time_limit = trial.suggest_int('time_limit', 60 * 60 * 2, 60 * 60 * 24)
|
||||
time_limit = trial.suggest_int('time_limit', 60 * 60 * 12, 60 * 60 * 24)
|
||||
|
||||
triple_barrier_conf = TripleBarrierConf(
|
||||
stop_loss=Decimal(stop_loss), take_profit=Decimal(take_profit),
|
||||
|
||||
Reference in New Issue
Block a user