Skip to content

Instantly share code, notes, and snippets.

@garyzava
Last active April 28, 2025 10:49
Show Gist options
  • Save garyzava/090317be75ec78ea927f3bda2ab677bd to your computer and use it in GitHub Desktop.
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
"""
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