Last active
April 28, 2025 10:49
-
-
Save garyzava/090317be75ec78ea927f3bda2ab677bd to your computer and use it in GitHub Desktop.
This script implements a cross-exchange market making (XEMM) strategy using the hummingbot V2 framework
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Author: Gary & Chris | |
Date: 2025-04-26 | |
Version: 1.0 | |
Description: | |
This script implements a cross-exchange market making (XEMM) strategy | |
It uses Bollinger Bands Width (BBW) as a volatility indicator to adjust the strategy's behavior | |
It uses the Hummingbot V2 framework to manage the strategy | |
It is designed to be used with two different exchanges (maker and taker) in a cross-exchange market making scenario, | |
managing inventory and profitability | |
This XEMM strategy first places a limit order on the maker exchange (iliquid exchange) | |
Then it waits for the maker order to be filled | |
Once the maker order is filled, it places a market order on the taker exchange (liquid exchange) | |
The strategy is designed to provide liquidity on the maker exchange and hedge the position on the taker exchange | |
""" | |
import os | |
from decimal import Decimal | |
from typing import Dict, List, Set, Optional | |
from pydantic import Field | |
from hummingbot.client.config.config_data_types import ClientFieldData | |
from hummingbot.connector.connector_base import ConnectorBase, TradeType | |
from hummingbot.core.data_type.common import OrderType, PriceType | |
from hummingbot.data_feed.candles_feed.data_types import CandlesConfig | |
from hummingbot.strategy.strategy_v2_base import StrategyV2Base, StrategyV2ConfigBase | |
from hummingbot.strategy_v2.executors.data_types import ConnectorPair | |
from hummingbot.strategy_v2.executors.xemm_executor.data_types import XEMMExecutorConfig | |
from hummingbot.strategy_v2.models.executor_actions import CreateExecutorAction, ExecutorAction, StopExecutorAction | |
from hummingbot.core.data_type.order_candidate import OrderCandidate | |
class V2XEMMConfig(StrategyV2ConfigBase): | |
script_file_name: str = Field(default_factory=lambda: os.path.basename(__file__)) | |
candles_config: List[CandlesConfig] = [] | |
controllers_config: List[str] = [] | |
markets: Dict[str, Set[str]] = {} | |
# XEMM parameters | |
maker_connector: str = Field( | |
default="binance", | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the maker connector: ", | |
prompt_on_new=True | |
)) | |
maker_trading_pair: str = Field( | |
default="ETH-USDT", | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the maker trading pair: ", | |
prompt_on_new=True | |
)) | |
taker_connector: str = Field( | |
default="htx", | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the taker connector: ", | |
prompt_on_new=True | |
)) | |
taker_trading_pair: str = Field( | |
default="ETH-USDT", | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the taker trading pair: ", | |
prompt_on_new=True | |
)) | |
target_profitability: Decimal = Field( | |
default=0.006, | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the target profitability: ", | |
prompt_on_new=True | |
)) | |
min_profitability: Decimal = Field( | |
default=0.003, | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the minimum profitability: ", | |
prompt_on_new=True | |
)) | |
max_profitability: Decimal = Field( | |
default=0.008, | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the maximum profitability: ", | |
prompt_on_new=True | |
)) | |
order_amount_quote: Decimal = Field( | |
default=100, | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the order amount in quote asset: ", | |
prompt_on_new=True | |
)) | |
bb_length: int = Field( | |
default=20, | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the Bollinger Bands period: ", | |
prompt_on_new=True | |
)) | |
bb_std: float = Field( | |
default=2.0, | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the Bollinger Bands standard deviation multiplier: ", | |
prompt_on_new=True | |
)) | |
candles_interval: str = Field( | |
default="1m", | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the candles interval for volatility calculation (e.g. 1m, 1h, 1d): ", | |
prompt_on_new=True | |
)) | |
min_bbw_threshold: float = Field( | |
default=0.01, # 1% | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the minimum Bollinger Bands Width threshold (as decimal, e.g. 0.01 for 1%): ", | |
prompt_on_new=True | |
)) | |
max_bbw_threshold: float = Field( | |
default=0.08, # 8% | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the maximum Bollinger Bands Width threshold (as decimal, e.g. 0.08 for 8%): ", | |
prompt_on_new=True | |
)) | |
high_volatility_threshold: float = Field( | |
default=0.06, # 6% | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the high volatility threshold for spread widening (as decimal, e.g. 0.06 for 6%): ", | |
prompt_on_new=True | |
)) | |
spread_multiplier_high_vol: float = Field( | |
default=1.5, | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the spread multiplier to use during high volatility: ", | |
prompt_on_new=True | |
)) | |
assumed_slippage: Decimal = Field( | |
default=0.001, # 0.1% | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the assumed slippage (as decimal, e.g. 0.001 for 0.1%): ", | |
prompt_on_new=True | |
)) | |
inventory_skew_enabled: bool = Field( | |
default=True, | |
client_data=ClientFieldData( | |
prompt=lambda e: "Do you want to enable inventory skew management? (True/False): ", | |
prompt_on_new=True | |
)) | |
target_base_pct: Decimal = Field( | |
default=0.5, | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the target base asset percentage (e.g. 0.5 for 50%): ", | |
prompt_on_new=True | |
)) | |
inventory_range_multiplier: Decimal = Field( | |
default=1.5, | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enter the inventory range multiplier: ", | |
prompt_on_new=True | |
)) | |
# Candles configuration for volatility analysis | |
candles_connector: str = Field( | |
default="binance", | |
client_data=ClientFieldData( | |
prompt_on_new=True, | |
prompt=lambda e: "Enter the connector for the candles data, leave empty to use the same exchange as the connector: ", ) | |
) | |
candles_trading_pair: str = Field( | |
default="ETH-USDT", | |
client_data=ClientFieldData( | |
prompt_on_new=True, | |
prompt=lambda e: "Enter the trading pair for the candles data, leave empty to use the same trading pair as the connector: ", ) | |
) | |
# New flags for optional checks | |
enable_balance_check: bool = Field( | |
default=True, | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enable balance validation before placing orders? (True/False): ", | |
prompt_on_new=True | |
)) | |
enable_profitability_check: bool = Field( | |
default=True, | |
client_data=ClientFieldData( | |
prompt=lambda e: "Enable profitability validation before placing orders? (True/False): ", | |
prompt_on_new=True | |
)) | |
enable_volatility_management: bool = True | |
# enable_volatility_management: bool = Field( | |
# default=True, | |
# client_data=ClientFieldData( | |
# prompt=lambda e: "Enable volatility management? (True/False): ", | |
# prompt_on_new=True | |
# )) | |
class V2XEMM(StrategyV2Base): | |
@classmethod | |
def init_markets(cls, config: V2XEMMConfig): | |
cls.markets = {config.maker_connector: {config.maker_trading_pair}, config.taker_connector: {config.taker_trading_pair}} | |
def __init__(self, connectors: Dict[str, ConnectorBase], config: V2XEMMConfig): | |
super().__init__(connectors, config) | |
self.config = config | |
self._candles = None | |
self._bbw = None | |
self._market_condition = "NORMAL" # NORMAL, HIGH_VOL, EXTREME_VOL | |
self._last_status_message = "" | |
self.max_records = 200 # Default max records for candles | |
# GZ: Ensure candles config is set | |
if len(self.config.candles_config) == 0: | |
self.config.candles_config = [CandlesConfig( | |
connector=config.candles_connector, | |
trading_pair=config.candles_trading_pair, | |
candles_interval=config.candles_interval, | |
max_records=self.max_records | |
)] | |
# GZ: call start_network to initialize candles | |
if self.config.candles_config: | |
self._candles = self.market_data_provider.get_candles_feed(self.config.candles_config[0]) | |
if self._candles: | |
self._candles.start() | |
self.logger().info(f"Candles config: {self.config.candles_config}") | |
# async def start_network(self): | |
# self.logger().info("V2XEMM start_network method called") # GZ | |
# await super().start_network() | |
# Initialize candles | |
# if self.config.candles_config: | |
# self._candles = self.market_data_provider.get_candles_feed(self.config.candles_config[0]) | |
# if self._candles: | |
# self._candles.start() | |
# async def stop_network(self): | |
# self.logger().info("V2XEMM stop_network method called") # GZ | |
# Stop candles feed | |
# if self._candles: | |
# self._candles.stop() | |
# await super().stop_network() | |
def calculate_bollinger_bands(self) -> Optional[float]: | |
"""Calculate Bollinger Bands Width (BBW) as volatility indicator""" | |
if self._candles is None or not self._candles.ready: | |
self.logger().warning("Candles are not ready yet.") | |
return None | |
try: | |
df = self._candles.candles_df.copy() | |
self.logger().info(f"Candles dataframe has {len(df)} rows. Last timestamp: {df['timestamp'].iloc[-1] if not df.empty else 'N/A'}") #GZ print | |
if len(df) < self.config.bb_length: | |
self.logger().warning(f"Not enough candles data. Required {self.config.bb_length}, got {len(df)}") | |
return None | |
# Calculate simple moving average | |
df['sma'] = df['close'].rolling(window=self.config.bb_length).mean() | |
# Calculate standard deviation | |
df['stdev'] = df['close'].rolling(window=self.config.bb_length).std() | |
# Calculate upper and lower bands | |
df['upper_band'] = df['sma'] + (df['stdev'] * self.config.bb_std) | |
df['lower_band'] = df['sma'] - (df['stdev'] * self.config.bb_std) | |
# Calculate BBW as percentage of price | |
df['bbw'] = (df['upper_band'] - df['lower_band']) / df['sma'] | |
bbw = df['bbw'].iloc[-1] | |
return float(bbw) | |
except Exception as e: | |
self.logger().error(f"Error calculating Bollinger Bands: {e}") | |
return None | |
def update_market_condition(self): | |
"""Update market condition based on BBW""" | |
bbw = self.calculate_bollinger_bands() | |
if bbw is None: | |
self._market_condition = "NORMAL" # Default to normal if can't calculate | |
return | |
self._bbw = bbw | |
# Check market conditions based on BBW thresholds | |
if bbw < self.config.min_bbw_threshold or bbw > self.config.max_bbw_threshold: | |
self._market_condition = "EXTREME_VOL" # Extremely low or high volatility - potential breakout | |
elif bbw > self.config.high_volatility_threshold: | |
self._market_condition = "HIGH_VOL" # High volatility - widen spreads | |
else: | |
self._market_condition = "NORMAL" # Normal market conditions | |
# GZ: logging | |
self._last_status_message = f"Market condition updated: {self._market_condition} (BBW: {bbw:.4f})" | |
self.logger().info(f"Market condition updated: {self._market_condition} (BBW: {bbw:.4f})") | |
# GZ: For debugging purposes. Reset to normal after processing | |
# self._market_condition = "NORMAL" | |
def validate_sufficient_balance(self, connector_name: str, trading_pair: str, side: TradeType, amount: Decimal) -> bool: | |
"""Check if there's sufficient balance for the order | |
If inventary is balanced in both sides, orders can be placed at the requested size without being adjusted downward due to balance constraints. | |
For contrast, there is insufficient balance, the function will log where the adjusted amount is significantly smaller than the original amount, or even zero. | |
""" | |
# Skip balance check if disabled | |
if not self.config.enable_balance_check: | |
return True | |
connector = self.connectors[connector_name] | |
base, quote = trading_pair.split("-") | |
# For buy orders, check quote balance; for sell orders, check base balance | |
asset = quote if side == TradeType.BUY else base | |
available_balance = connector.get_available_balance(asset) | |
# GZ available_balance not being used | |
self.logger().info(f"Available balance for {asset} on {connector_name}: {available_balance:.6f}") | |
# Create an order candidate to accurately estimate required balance with fees | |
mid_price = self.market_data_provider.get_price_by_type(connector_name, trading_pair, PriceType.MidPrice) | |
order_candidate = OrderCandidate( | |
trading_pair=trading_pair, | |
is_maker=True, | |
order_type=OrderType.LIMIT, | |
order_side=side, | |
amount=amount, | |
price=mid_price, | |
) | |
# Adjust order candidate with budget checker | |
adjusted_candidate = connector.budget_checker.adjust_candidate(order_candidate, all_or_none=True) | |
# GZ logging - start | |
# Check if the adjusted amount is less than or equal to available balance | |
# Log the adjusted amount | |
self.logger().info(f"Adjusted amount for {asset} on {connector_name}: {adjusted_candidate.amount:.6f}") | |
# Log amount | |
self.logger().info(f"Original amount for {asset} on {connector_name}: {amount:.6f}") | |
if adjusted_candidate.amount > available_balance: | |
self.logger().warning(f"Insufficient {asset} balance on {connector_name} for {trading_pair} {side} order") | |
return False | |
# Check if the adjusted amount is less than or equal to the original amount | |
if adjusted_candidate.amount > amount: | |
self.logger().warning(f"Adjusted amount {adjusted_candidate.amount} is greater than original amount {amount}") | |
return False | |
# GZ logging - end | |
# If the amount becomes zero after adjustment, there's insufficient balance | |
if adjusted_candidate.amount == Decimal("0"): | |
self.logger().warning(f"Insufficient {asset} balance on {connector_name} for {trading_pair} {side} order") | |
return False | |
return True | |
def get_inventory_skew_multiplier(self, connector_name: str, trading_pair: str, side: TradeType) -> Decimal: | |
"""Calculate inventory skew multiplier based on current inventory ratio""" | |
if not self.config.inventory_skew_enabled: | |
return Decimal("1.0") # No skew if disabled | |
connector = self.connectors[connector_name] | |
base, quote = trading_pair.split("-") | |
base_balance = connector.get_balance(base) | |
quote_balance = connector.get_balance(quote) | |
mid_price = self.market_data_provider.get_price_by_type(connector_name, trading_pair, PriceType.MidPrice) | |
# Calculate total portfolio value in quote terms | |
total_value = base_balance * mid_price + quote_balance | |
if total_value == Decimal("0"): | |
return Decimal("1.0") # No assets, no skew | |
# Current base asset ratio | |
current_base_pct = (base_balance * mid_price) / total_value | |
# Calculate distance from target percentage | |
inventory_distance = current_base_pct - self.config.target_base_pct | |
# Adjust skew based on side | |
if side == TradeType.BUY: | |
# If we have too much base asset, reduce buy size | |
if inventory_distance > Decimal("0"): | |
return max(Decimal("0.1"), Decimal("1.0") - (inventory_distance * self.config.inventory_range_multiplier)) | |
# If we have too little base asset, increase buy size | |
else: | |
return min(Decimal("2.0"), Decimal("1.0") + (abs(inventory_distance) * self.config.inventory_range_multiplier)) | |
else: # SELL | |
# If we have too much base asset, increase sell size | |
if inventory_distance > Decimal("0"): | |
return min(Decimal("2.0"), Decimal("1.0") + (inventory_distance * self.config.inventory_range_multiplier)) | |
# If we have too little base asset, reduce sell size | |
else: | |
return max(Decimal("0.1"), Decimal("1.0") - (abs(inventory_distance) * self.config.inventory_range_multiplier)) | |
def calculate_profitability_after_costs(self, maker_connector: str, maker_trading_pair: str, | |
taker_connector: str, taker_trading_pair: str, | |
maker_side: TradeType) -> Optional[Decimal]: | |
"""Calculate expected profitability after fees and slippage""" | |
# Skip profitability check if disabled | |
if not self.config.enable_profitability_check: | |
return Decimal("1.0") # Return a value that will pass any threshold check | |
try: | |
# Get current prices | |
maker_price = self.market_data_provider.get_price_by_type(maker_connector, maker_trading_pair, | |
PriceType.MidPrice) | |
taker_price = self.market_data_provider.get_price_by_type(taker_connector, taker_trading_pair, | |
PriceType.MidPrice) | |
# Get maker fee | |
maker_connector_obj = self.connectors[maker_connector] | |
maker_fee = maker_connector_obj.get_fee( | |
base_currency=maker_trading_pair.split("-")[0], | |
quote_currency=maker_trading_pair.split("-")[1], | |
order_type=OrderType.LIMIT, | |
order_side=maker_side, | |
amount=self.config.order_amount_quote / maker_price, | |
price=maker_price, | |
is_maker=True | |
).percent | |
# Get taker fee | |
taker_connector_obj = self.connectors[taker_connector] | |
taker_fee = taker_connector_obj.get_fee( | |
base_currency=taker_trading_pair.split("-")[0], | |
quote_currency=taker_trading_pair.split("-")[1], | |
order_type=OrderType.MARKET, | |
order_side=TradeType.SELL if maker_side == TradeType.BUY else TradeType.BUY, | |
amount=self.config.order_amount_quote / taker_price, | |
price=taker_price, | |
is_maker=False | |
).percent | |
# Calculate effective prices after fees and slippage | |
if maker_side == TradeType.BUY: | |
# When buying on maker and selling on taker | |
effective_buy_price = maker_price * (Decimal("1") + maker_fee) | |
effective_sell_price = taker_price * (Decimal("1") - taker_fee - self.config.assumed_slippage) | |
profitability = (effective_sell_price - effective_buy_price) / effective_buy_price | |
else: | |
# When selling on maker and buying on taker | |
effective_sell_price = maker_price * (Decimal("1") - maker_fee) | |
effective_buy_price = taker_price * (Decimal("1") + taker_fee + self.config.assumed_slippage) | |
profitability = (effective_sell_price - effective_buy_price) / effective_buy_price | |
return profitability | |
except Exception as e: | |
self.logger().error(f"Error calculating profitability after costs: {e}") | |
return None | |
def determine_executor_actions(self) -> List[ExecutorAction]: | |
""" | |
This function validates before executing an order: | |
- For inventory skew adjustment (which affects order sizing), the code is only considering balances on the maker exchange | |
- For balance checks before placing orders, it's only checking balances on the maker exchange | |
- The status display shows balances from both exchanges for informational purposes | |
This approach makes sense for a XEMM strategy to manage inventory on the maker side where we are providing liquidity, | |
as it might not be optimal for the strategy to consider the total inventory across both exchanges when making decisions about order sizes. | |
""" | |
executor_actions = [] | |
# Update market condition based on BBW | |
#self.update_market_condition() | |
# GZ: Update market condition based on BBW if volatility management is enabled | |
if self.config.enable_volatility_management: | |
self.update_market_condition() | |
else: | |
self._market_condition = "NORMAL" # Force normal condition if volatility management is disabled | |
self._last_status_message = "Volatility management is disabled. Using normal market condition." | |
self.logger().info("Volatility management is disabled. Using normal market condition.") | |
# Get all active executors | |
all_executors = self.get_all_executors() | |
# Handle extreme volatility - cancel all orders and don't create new ones | |
if self._market_condition == "EXTREME_VOL": | |
self._last_status_message = f"Extreme volatility detected (BBW: {self._bbw:.4f}). Canceling all orders and standing by." | |
# Stop all active executors | |
for executor in all_executors: | |
if not executor.is_done: | |
executor_actions.append(StopExecutorAction(executor_id=executor.id)) | |
return executor_actions | |
# Get current mid price for calculating order amount | |
mid_price = self.market_data_provider.get_price_by_type( | |
self.config.maker_connector, | |
self.config.maker_trading_pair, | |
PriceType.MidPrice | |
) | |
# Filter active executors | |
active_buy_executors = self.filter_executors( | |
executors=all_executors, | |
filter_func=lambda e: not e.is_done and e.config.maker_side == TradeType.BUY | |
) | |
active_sell_executors = self.filter_executors( | |
executors=all_executors, | |
filter_func=lambda e: not e.is_done and e.config.maker_side == TradeType.SELL | |
) | |
# Calculate base order amount | |
base_order_amount = self.config.order_amount_quote / mid_price | |
# Adjust target profitability based on market conditions | |
target_profitability = self.config.target_profitability | |
min_profitability = self.config.min_profitability | |
max_profitability = self.config.max_profitability | |
if self._market_condition == "HIGH_VOL": | |
# Widen spreads during high volatility | |
target_profitability = target_profitability * Decimal(str(self.config.spread_multiplier_high_vol)) | |
min_profitability = min_profitability * Decimal(str(self.config.spread_multiplier_high_vol)) | |
max_profitability = max_profitability * Decimal(str(self.config.spread_multiplier_high_vol)) | |
self._last_status_message = f"High volatility detected (BBW: {self._bbw:.4f}). Widening spreads." | |
else: | |
# Handle case when BBW is None | |
if self._bbw is None: | |
self._last_status_message = "Waiting for candles data to be ready..." | |
else: | |
self._last_status_message = f"Normal market conditions (BBW: {self._bbw:.4f})." | |
# Check for buy side execution | |
if len(active_buy_executors) == 0: | |
# Calculate inventory skew multiplier | |
buy_skew_multiplier = self.get_inventory_skew_multiplier( | |
self.config.maker_connector, | |
self.config.maker_trading_pair, | |
TradeType.BUY | |
) | |
# Adjust order amount based on inventory skew | |
adjusted_amount = base_order_amount * buy_skew_multiplier | |
# Validate sufficient balance | |
has_sufficient_balance = self.validate_sufficient_balance( | |
self.config.maker_connector, | |
self.config.maker_trading_pair, | |
TradeType.BUY, | |
adjusted_amount | |
) | |
# Calculate expected profitability after costs | |
profitability = self.calculate_profitability_after_costs( | |
self.config.maker_connector, | |
self.config.maker_trading_pair, | |
self.config.taker_connector, | |
self.config.taker_trading_pair, | |
TradeType.BUY | |
) | |
# Only create buy executor if there's enough balance and potential profit | |
#if has_sufficient_balance and profitability is not None and profitability > min_profitability: | |
# Only create buy executor if checks pass or are disabled | |
# GZ -start | |
create_executor = True | |
if self.config.enable_balance_check and not has_sufficient_balance: | |
create_executor = False | |
self.logger().info("Not creating buy executor due to insufficient balance") | |
if self.config.enable_profitability_check and (profitability is None or profitability <= min_profitability): | |
create_executor = False | |
self.logger().info(f"Not creating buy executor due to low profitability: {profitability}") | |
if create_executor: | |
# GZ -end | |
config = XEMMExecutorConfig( | |
timestamp=self.current_timestamp, | |
buying_market=ConnectorPair(connector_name=self.config.maker_connector, | |
trading_pair=self.config.maker_trading_pair), | |
selling_market=ConnectorPair(connector_name=self.config.taker_connector, | |
trading_pair=self.config.taker_trading_pair), | |
maker_side=TradeType.BUY, | |
order_amount=adjusted_amount, | |
min_profitability=min_profitability, | |
target_profitability=target_profitability, | |
max_profitability=max_profitability | |
) | |
executor_actions.append(CreateExecutorAction(executor_config=config)) | |
# Check for sell side execution | |
if len(active_sell_executors) == 0: | |
# Calculate inventory skew multiplier | |
sell_skew_multiplier = self.get_inventory_skew_multiplier( | |
self.config.maker_connector, | |
self.config.maker_trading_pair, | |
TradeType.SELL | |
) | |
# Adjust order amount based on inventory skew | |
adjusted_amount = base_order_amount * sell_skew_multiplier | |
# Validate sufficient balance | |
has_sufficient_balance = self.validate_sufficient_balance( | |
self.config.maker_connector, | |
self.config.maker_trading_pair, | |
TradeType.SELL, | |
adjusted_amount | |
) | |
# Calculate expected profitability after costs | |
profitability = self.calculate_profitability_after_costs( | |
self.config.maker_connector, | |
self.config.maker_trading_pair, | |
self.config.taker_connector, | |
self.config.taker_trading_pair, | |
TradeType.SELL | |
) | |
# Only create sell executor if there's enough balance and potential profit | |
# GZ to review | |
if has_sufficient_balance and profitability is not None and profitability > min_profitability: | |
config = XEMMExecutorConfig( | |
timestamp=self.current_timestamp, | |
buying_market=ConnectorPair(connector_name=self.config.taker_connector, | |
trading_pair=self.config.taker_trading_pair), | |
selling_market=ConnectorPair(connector_name=self.config.maker_connector, | |
trading_pair=self.config.maker_trading_pair), | |
maker_side=TradeType.SELL, | |
order_amount=adjusted_amount, | |
min_profitability=min_profitability, | |
target_profitability=target_profitability, | |
max_profitability=max_profitability | |
) | |
executor_actions.append(CreateExecutorAction(executor_config=config)) | |
return executor_actions | |
def format_status(self) -> str: | |
"""Format status message with all relevant information""" | |
original_status = super().format_status() | |
lines = [] | |
lines.append("\n===== XEMM Strategy Status =====") | |
# GZ: Add volatility management status | |
lines.append(f"Volatility Management: {'Enabled' if self.config.enable_volatility_management else 'Disabled'}") | |
lines.append(f"Market Condition: {self._market_condition}") | |
if self._bbw is not None: | |
lines.append(f"Bollinger Band Width: {self._bbw:.4f}") | |
lines.append(f"Status: {self._last_status_message}") | |
# GZ - start | |
# Add check configuration information | |
lines.append(f"Balance Check: {'Enabled' if self.config.enable_balance_check else 'Disabled'}") | |
lines.append(f"Profitability Check: {'Enabled' if self.config.enable_profitability_check else 'Disabled'}") | |
# GZ - end | |
# Add inventory information | |
maker_connector = self.connectors[self.config.maker_connector] | |
taker_connector = self.connectors[self.config.taker_connector] | |
maker_base, maker_quote = self.config.maker_trading_pair.split("-") | |
taker_base, taker_quote = self.config.taker_trading_pair.split("-") | |
maker_base_bal = maker_connector.get_balance(maker_base) | |
maker_quote_bal = maker_connector.get_balance(maker_quote) | |
taker_base_bal = taker_connector.get_balance(taker_base) | |
taker_quote_bal = taker_connector.get_balance(taker_quote) | |
lines.append("\n----- Inventory -----") | |
lines.append(f"{self.config.maker_connector}: {maker_base}={maker_base_bal:.6f}, {maker_quote}={maker_quote_bal:.6f}") | |
lines.append(f"{self.config.taker_connector}: {taker_base}={taker_base_bal:.6f}, {taker_quote}={taker_quote_bal:.6f}") | |
# Display active executors | |
lines.append("\n----- Active Executors -----") | |
active_executors = self.get_all_executors() | |
if not active_executors: | |
lines.append("No active executors") | |
else: | |
for idx, ex in enumerate(active_executors): | |
if not ex.is_done: | |
#lines.append(f"Executor {idx+1}: {ex.to_format_status()}") | |
# GZ | |
lines.append(f"Executor {idx+1}: {ex.config.maker_side} {ex.config.order_amount:.6f} {ex.config.buying_market.trading_pair} @ {ex.config.buying_market.connector_name}") | |
return f"{original_status}\n" + '\n'.join(lines) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment