mirror of
https://github.com/d0zingcat/deploy.git
synced 2026-05-14 15:09:44 +00:00
493 lines
24 KiB
Python
493 lines
24 KiB
Python
from decimal import Decimal
|
|
from typing import Dict, List, Set, Union
|
|
|
|
import pandas_ta as ta # noqa: F401
|
|
from pydantic import Field, field_validator
|
|
|
|
from hummingbot.core.data_type.common import OrderType, PositionMode, PriceType, TradeType
|
|
from hummingbot.data_feed.candles_feed.data_types import CandlesConfig
|
|
from hummingbot.strategy_v2.controllers import ControllerBase, ControllerConfigBase
|
|
from hummingbot.strategy_v2.executors.data_types import ConnectorPair
|
|
from hummingbot.strategy_v2.executors.grid_executor.data_types import GridExecutorConfig
|
|
from hummingbot.strategy_v2.executors.position_executor.data_types import TripleBarrierConfig
|
|
from hummingbot.strategy_v2.models.executor_actions import CreateExecutorAction, StopExecutorAction
|
|
from hummingbot.strategy_v2.models.executors_info import ExecutorInfo
|
|
|
|
|
|
class QGAConfig(ControllerConfigBase):
|
|
controller_name: str = "quantum_grid_allocator"
|
|
candles_config: List[CandlesConfig] = []
|
|
|
|
# Portfolio allocation zones
|
|
long_only_threshold: Decimal = Field(default=Decimal("0.2"), json_schema_extra={"is_updatable": True})
|
|
short_only_threshold: Decimal = Field(default=Decimal("0.2"), json_schema_extra={"is_updatable": True})
|
|
hedge_ratio: Decimal = Field(default=Decimal("2"), json_schema_extra={"is_updatable": True})
|
|
|
|
# Grid allocation multipliers
|
|
base_grid_value_pct: Decimal = Field(default=Decimal("0.08"), json_schema_extra={"is_updatable": True})
|
|
max_grid_value_pct: Decimal = Field(default=Decimal("0.15"), json_schema_extra={"is_updatable": True})
|
|
|
|
# Order frequency settings
|
|
safe_extra_spread: Decimal = Field(default=Decimal("0.0001"), json_schema_extra={"is_updatable": True})
|
|
favorable_order_frequency: int = Field(default=2, json_schema_extra={"is_updatable": True})
|
|
unfavorable_order_frequency: int = Field(default=5, json_schema_extra={"is_updatable": True})
|
|
max_orders_per_batch: int = Field(default=1, json_schema_extra={"is_updatable": True})
|
|
|
|
# Portfolio allocation
|
|
portfolio_allocation: Dict[str, Decimal] = Field(
|
|
default={
|
|
"SOL": Decimal("0.50"), # 50%
|
|
},
|
|
json_schema_extra={"is_updatable": True})
|
|
# Grid parameters
|
|
grid_range: Decimal = Field(default=Decimal("0.002"), json_schema_extra={"is_updatable": True})
|
|
tp_sl_ratio: Decimal = Field(default=Decimal("0.8"), json_schema_extra={"is_updatable": True})
|
|
min_order_amount: Decimal = Field(default=Decimal("5"), json_schema_extra={"is_updatable": True})
|
|
# Risk parameters
|
|
max_deviation: Decimal = Field(default=Decimal("0.05"), json_schema_extra={"is_updatable": True})
|
|
max_open_orders: int = Field(default=2, json_schema_extra={"is_updatable": True})
|
|
# Exchange settings
|
|
connector_name: str = "binance"
|
|
leverage: int = 1
|
|
position_mode: PositionMode = PositionMode.HEDGE
|
|
quote_asset: str = "FDUSD"
|
|
fee_asset: str = "BNB"
|
|
# Grid price multipliers
|
|
min_spread_between_orders: Decimal = Field(
|
|
default=Decimal("0.0001"), # 0.01% between orders
|
|
json_schema_extra={"is_updatable": True})
|
|
grid_tp_multiplier: Decimal = Field(
|
|
default=Decimal("0.0001"), # 0.2% take profit
|
|
json_schema_extra={"is_updatable": True})
|
|
# Grid safety parameters
|
|
limit_price_spread: Decimal = Field(
|
|
default=Decimal("0.001"), # 0.1% spread for limit price
|
|
json_schema_extra={"is_updatable": True})
|
|
activation_bounds: Decimal = Field(
|
|
default=Decimal("0.0002"), # Activation bounds for orders
|
|
json_schema_extra={"is_updatable": True})
|
|
bb_lenght: int = 100
|
|
bb_std_dev: float = 2.0
|
|
interval: str = "1s"
|
|
dynamic_grid_range: bool = Field(default=False, json_schema_extra={"is_updatable": True})
|
|
show_terminated_details: bool = False
|
|
|
|
@property
|
|
def quote_asset_allocation(self) -> Decimal:
|
|
"""Calculate the implicit quote asset (FDUSD) allocation"""
|
|
return Decimal("1") - sum(self.portfolio_allocation.values())
|
|
|
|
@field_validator("portfolio_allocation")
|
|
@classmethod
|
|
def validate_allocation(cls, v):
|
|
total = sum(v.values())
|
|
if total >= Decimal("1"):
|
|
raise ValueError(f"Total allocation {total} exceeds or equals 100%. Must leave room for FDUSD allocation.")
|
|
if "FDUSD" in v:
|
|
raise ValueError("FDUSD should not be explicitly allocated as it is the quote asset")
|
|
return v
|
|
|
|
def update_markets(self, markets: Dict[str, Set[str]]) -> Dict[str, Set[str]]:
|
|
if self.connector_name not in markets:
|
|
markets[self.connector_name] = set()
|
|
for asset in self.portfolio_allocation:
|
|
markets[self.connector_name].add(f"{asset}-{self.quote_asset}")
|
|
return markets
|
|
|
|
|
|
class QuantumGridAllocator(ControllerBase):
|
|
def __init__(self, config: QGAConfig, *args, **kwargs):
|
|
self.config = config
|
|
self.metrics = {}
|
|
# Track unfavorable grid IDs
|
|
self.unfavorable_grid_ids = set()
|
|
# Track held positions from unfavorable grids
|
|
self.unfavorable_positions = {
|
|
f"{asset}-{config.quote_asset}": {
|
|
'long': {'size': Decimal('0'), 'value': Decimal('0'), 'weighted_price': Decimal('0')},
|
|
'short': {'size': Decimal('0'), 'value': Decimal('0'), 'weighted_price': Decimal('0')}
|
|
}
|
|
for asset in config.portfolio_allocation
|
|
}
|
|
self.config.candles_config = [CandlesConfig(
|
|
connector=config.connector_name,
|
|
trading_pair=trading_pair + "-" + config.quote_asset,
|
|
interval=config.interval,
|
|
max_records=config.bb_lenght + 100
|
|
) for trading_pair in config.portfolio_allocation.keys()]
|
|
super().__init__(config, *args, **kwargs)
|
|
self.initialize_rate_sources()
|
|
|
|
def initialize_rate_sources(self):
|
|
fee_pair = ConnectorPair(connector_name=self.config.connector_name, trading_pair=f"{self.config.fee_asset}-{self.config.quote_asset}")
|
|
self.market_data_provider.initialize_rate_sources([fee_pair])
|
|
|
|
async def update_processed_data(self):
|
|
# Get the bb width to use it as the range for the grid
|
|
for asset in self.config.portfolio_allocation:
|
|
trading_pair = f"{asset}-{self.config.quote_asset}"
|
|
candles = self.market_data_provider.get_candles_df(
|
|
connector_name=self.config.connector_name,
|
|
trading_pair=trading_pair,
|
|
interval=self.config.interval,
|
|
max_records=self.config.bb_lenght + 100
|
|
)
|
|
if len(candles) == 0:
|
|
bb_width = self.config.grid_range
|
|
else:
|
|
bb = ta.bbands(candles["close"], length=self.config.bb_lenght, std=self.config.bb_std_dev)
|
|
bb_width = bb[f"BBB_{self.config.bb_lenght}_{self.config.bb_std_dev}"].iloc[-1] / 100
|
|
self.processed_data[trading_pair] = {
|
|
"bb_width": bb_width
|
|
}
|
|
|
|
def update_portfolio_metrics(self):
|
|
"""
|
|
Calculate theoretical vs actual portfolio allocations
|
|
"""
|
|
metrics = {
|
|
"theoretical": {},
|
|
"actual": {},
|
|
"difference": {},
|
|
}
|
|
|
|
# Get real balances and calculate total portfolio value
|
|
quote_balance = self.market_data_provider.get_balance(self.config.connector_name, self.config.quote_asset)
|
|
total_value_quote = quote_balance
|
|
|
|
# Calculate actual allocations including positions
|
|
for asset in self.config.portfolio_allocation:
|
|
trading_pair = f"{asset}-{self.config.quote_asset}"
|
|
price = self.get_mid_price(trading_pair)
|
|
# Get balance and add any position from active grid
|
|
balance = self.market_data_provider.get_balance(self.config.connector_name, asset)
|
|
value = balance * price
|
|
total_value_quote += value
|
|
metrics["actual"][asset] = value
|
|
# Calculate theoretical allocations and differences
|
|
for asset in self.config.portfolio_allocation:
|
|
theoretical_value = total_value_quote * self.config.portfolio_allocation[asset]
|
|
metrics["theoretical"][asset] = theoretical_value
|
|
metrics["difference"][asset] = metrics["actual"][asset] - theoretical_value
|
|
# Add quote asset metrics
|
|
metrics["actual"][self.config.quote_asset] = quote_balance
|
|
metrics["theoretical"][self.config.quote_asset] = total_value_quote * self.config.quote_asset_allocation
|
|
metrics["difference"][self.config.quote_asset] = quote_balance - metrics["theoretical"][self.config.quote_asset]
|
|
metrics["total_portfolio_value"] = total_value_quote
|
|
self.metrics = metrics
|
|
|
|
def get_active_grids_by_asset(self) -> Dict[str, List[ExecutorInfo]]:
|
|
"""Group active grids by asset using filter_executors"""
|
|
active_grids = {}
|
|
for asset in self.config.portfolio_allocation:
|
|
if asset == self.config.quote_asset:
|
|
continue
|
|
trading_pair = f"{asset}-{self.config.quote_asset}"
|
|
active_executors = self.filter_executors(
|
|
executors=self.executors_info,
|
|
filter_func=lambda e: (
|
|
e.is_active and
|
|
e.config.trading_pair == trading_pair
|
|
)
|
|
)
|
|
if active_executors:
|
|
active_grids[asset] = active_executors
|
|
return active_grids
|
|
|
|
def to_format_status(self) -> List[str]:
|
|
"""Generate a detailed status report with portfolio, grid, and position information"""
|
|
status_lines = []
|
|
total_value = self.metrics.get("total_portfolio_value", Decimal("0"))
|
|
# Portfolio Status
|
|
status_lines.append(f"Total Portfolio Value: ${total_value:,.2f}")
|
|
status_lines.append("")
|
|
status_lines.append("Portfolio Status:")
|
|
status_lines.append("-" * 80)
|
|
status_lines.append(
|
|
f"{'Asset':<8} | "
|
|
f"{'Actual':>10} | "
|
|
f"{'Target':>10} | "
|
|
f"{'Diff':>10} | "
|
|
f"{'Dev %':>8}"
|
|
)
|
|
status_lines.append("-" * 80)
|
|
# Show metrics for each asset
|
|
for asset in self.config.portfolio_allocation:
|
|
actual = self.metrics["actual"].get(asset, Decimal("0"))
|
|
theoretical = self.metrics["theoretical"].get(asset, Decimal("0"))
|
|
difference = self.metrics["difference"].get(asset, Decimal("0"))
|
|
deviation_pct = (difference / theoretical * 100) if theoretical != Decimal("0") else Decimal("0")
|
|
status_lines.append(
|
|
f"{asset:<8} | "
|
|
f"${actual:>9.2f} | "
|
|
f"${theoretical:>9.2f} | "
|
|
f"${difference:>+9.2f} | "
|
|
f"{deviation_pct:>+7.1f}%"
|
|
)
|
|
# Add quote asset metrics
|
|
quote_asset = self.config.quote_asset
|
|
actual = self.metrics["actual"].get(quote_asset, Decimal("0"))
|
|
theoretical = self.metrics["theoretical"].get(quote_asset, Decimal("0"))
|
|
difference = self.metrics["difference"].get(quote_asset, Decimal("0"))
|
|
deviation_pct = (difference / theoretical * 100) if theoretical != Decimal("0") else Decimal("0")
|
|
status_lines.append("-" * 80)
|
|
status_lines.append(
|
|
f"{quote_asset:<8} | "
|
|
f"${actual:>9.2f} | "
|
|
f"${theoretical:>9.2f} | "
|
|
f"${difference:>+9.2f} | "
|
|
f"{deviation_pct:>+7.1f}%"
|
|
)
|
|
# Active Grids Summary
|
|
active_grids = self.get_active_grids_by_asset()
|
|
if active_grids:
|
|
status_lines.append("")
|
|
status_lines.append("Active Grids:")
|
|
status_lines.append("-" * 140)
|
|
status_lines.append(
|
|
f"{'Asset':<8} {'Side':<6} | "
|
|
f"{'Total ($)':<10} {'Position':<10} {'Volume':<10} | "
|
|
f"{'PnL':<10} {'RPnL':<10} {'Fees':<10} | "
|
|
f"{'Start':<10} {'Current':<10} {'End':<10} {'Limit':<10}"
|
|
)
|
|
status_lines.append("-" * 140)
|
|
for asset, executors in active_grids.items():
|
|
for executor in executors:
|
|
config = executor.config
|
|
custom_info = executor.custom_info
|
|
trading_pair = config.trading_pair
|
|
current_price = self.get_mid_price(trading_pair)
|
|
# Get grid metrics
|
|
total_amount = Decimal(str(config.total_amount_quote))
|
|
position_size = Decimal(str(custom_info.get('position_size_quote', '0')))
|
|
volume = executor.filled_amount_quote
|
|
pnl = executor.net_pnl_quote
|
|
realized_pnl_quote = custom_info.get('realized_pnl_quote', Decimal('0'))
|
|
fees = executor.cum_fees_quote
|
|
status_lines.append(
|
|
f"{asset:<8} {config.side.name:<6} | "
|
|
f"${total_amount:<9.2f} ${position_size:<9.2f} ${volume:<9.2f} | "
|
|
f"${pnl:>+9.2f} ${realized_pnl_quote:>+9.2f} ${fees:>9.2f} | "
|
|
f"{config.start_price:<10.4f} {current_price:<10.4f} {config.end_price:<10.4f} {config.limit_price:<10.4f}"
|
|
)
|
|
|
|
status_lines.append("-" * 100 + "\n")
|
|
return status_lines
|
|
|
|
def tp_multiplier(self):
|
|
return self.config.tp_sl_ratio
|
|
|
|
def sl_multiplier(self):
|
|
return 1 - self.config.tp_sl_ratio
|
|
|
|
def determine_executor_actions(self) -> List[Union[CreateExecutorAction, StopExecutorAction]]:
|
|
actions = []
|
|
self.update_portfolio_metrics()
|
|
active_grids_by_asset = self.get_active_grids_by_asset()
|
|
for asset in self.config.portfolio_allocation:
|
|
if asset == self.config.quote_asset:
|
|
continue
|
|
trading_pair = f"{asset}-{self.config.quote_asset}"
|
|
# Check if there are any active grids for this asset
|
|
if asset in active_grids_by_asset:
|
|
self.logger().debug(f"Skipping {trading_pair} - Active grid exists")
|
|
continue
|
|
theoretical = self.metrics["theoretical"][asset]
|
|
difference = self.metrics["difference"][asset]
|
|
deviation = difference / theoretical if theoretical != Decimal("0") else Decimal("0")
|
|
mid_price = self.get_mid_price(trading_pair)
|
|
|
|
# Calculate dynamic grid value percentage based on deviation
|
|
abs_deviation = abs(deviation)
|
|
grid_value_pct = self.config.max_grid_value_pct if abs_deviation > self.config.max_deviation else self.config.base_grid_value_pct
|
|
|
|
self.logger().info(
|
|
f"{trading_pair} Grid Sizing - "
|
|
f"Deviation: {deviation:+.1%}, "
|
|
f"Grid Value %: {grid_value_pct:.1%}"
|
|
)
|
|
if self.config.dynamic_grid_range:
|
|
grid_range = Decimal(self.processed_data[trading_pair]["bb_width"])
|
|
else:
|
|
grid_range = self.config.grid_range
|
|
|
|
# Determine which zone we're in by normalizing the deviation over the theoretical allocation
|
|
if deviation < -self.config.long_only_threshold:
|
|
# Long-only zone - only create buy grids
|
|
if difference < Decimal("0"): # Only if we need to buy
|
|
grid_value = min(abs(difference), theoretical * grid_value_pct)
|
|
start_price = mid_price * (1 - grid_range * self.sl_multiplier())
|
|
end_price = mid_price * (1 + grid_range * self.tp_multiplier())
|
|
grid_action = self.create_grid_executor(
|
|
trading_pair=trading_pair,
|
|
side=TradeType.BUY,
|
|
start_price=start_price,
|
|
end_price=end_price,
|
|
grid_value=grid_value,
|
|
is_unfavorable=False
|
|
)
|
|
if grid_action is not None:
|
|
actions.append(grid_action)
|
|
elif deviation > self.config.short_only_threshold:
|
|
# Short-only zone - only create sell grids
|
|
if difference > Decimal("0"): # Only if we need to sell
|
|
grid_value = min(abs(difference), theoretical * grid_value_pct)
|
|
start_price = mid_price * (1 - grid_range * self.tp_multiplier())
|
|
end_price = mid_price * (1 + grid_range * self.sl_multiplier())
|
|
grid_action = self.create_grid_executor(
|
|
trading_pair=trading_pair,
|
|
side=TradeType.SELL,
|
|
start_price=start_price,
|
|
end_price=end_price,
|
|
grid_value=grid_value,
|
|
is_unfavorable=False
|
|
)
|
|
if grid_action is not None:
|
|
actions.append(grid_action)
|
|
else:
|
|
# we create a buy and a sell grid with higher range pct and the base grid value pct
|
|
# to hedge the position
|
|
grid_value = theoretical * grid_value_pct
|
|
if difference < Decimal("0"): # create a bigger buy grid and sell grid
|
|
# Create buy grid
|
|
start_price = mid_price * (1 - 2 * grid_range * self.sl_multiplier())
|
|
end_price = mid_price * (1 + grid_range * self.tp_multiplier())
|
|
buy_grid_action = self.create_grid_executor(
|
|
trading_pair=trading_pair,
|
|
side=TradeType.BUY,
|
|
start_price=start_price,
|
|
end_price=end_price,
|
|
grid_value=grid_value,
|
|
is_unfavorable=False
|
|
)
|
|
if buy_grid_action is not None:
|
|
actions.append(buy_grid_action)
|
|
# Create sell grid
|
|
start_price = mid_price * (1 - grid_range * self.tp_multiplier())
|
|
end_price = mid_price * (1 + 2 * grid_range * self.sl_multiplier())
|
|
sell_grid_action = self.create_grid_executor(
|
|
trading_pair=trading_pair,
|
|
side=TradeType.SELL,
|
|
start_price=start_price,
|
|
end_price=end_price,
|
|
grid_value=grid_value,
|
|
is_unfavorable=False
|
|
)
|
|
if sell_grid_action is not None:
|
|
actions.append(sell_grid_action)
|
|
if difference > Decimal("0"):
|
|
# Create sell grid
|
|
start_price = mid_price * (1 - 2 * grid_range * self.tp_multiplier())
|
|
end_price = mid_price * (1 + grid_range * self.sl_multiplier())
|
|
sell_grid_action = self.create_grid_executor(
|
|
trading_pair=trading_pair,
|
|
side=TradeType.SELL,
|
|
start_price=start_price,
|
|
end_price=end_price,
|
|
grid_value=grid_value,
|
|
is_unfavorable=False
|
|
)
|
|
if sell_grid_action is not None:
|
|
actions.append(sell_grid_action)
|
|
# Create buy grid
|
|
start_price = mid_price * (1 - grid_range * self.sl_multiplier())
|
|
end_price = mid_price * (1 + 2 * grid_range * self.tp_multiplier())
|
|
buy_grid_action = self.create_grid_executor(
|
|
trading_pair=trading_pair,
|
|
side=TradeType.BUY,
|
|
start_price=start_price,
|
|
end_price=end_price,
|
|
grid_value=grid_value,
|
|
is_unfavorable=False
|
|
)
|
|
if buy_grid_action is not None:
|
|
actions.append(buy_grid_action)
|
|
return actions
|
|
|
|
def create_grid_executor(
|
|
self,
|
|
trading_pair: str,
|
|
side: TradeType,
|
|
start_price: Decimal,
|
|
end_price: Decimal,
|
|
grid_value: Decimal,
|
|
is_unfavorable: bool = False
|
|
) -> CreateExecutorAction:
|
|
"""Creates a grid executor with dynamic sizing and range adjustments"""
|
|
# Get trading rules and minimum notional
|
|
trading_rules = self.market_data_provider.get_trading_rules(self.config.connector_name, trading_pair)
|
|
min_notional = max(
|
|
self.config.min_order_amount,
|
|
trading_rules.min_notional_size if trading_rules else Decimal("5.0")
|
|
)
|
|
# Add safety margin and check if grid value is sufficient
|
|
min_grid_value = min_notional * Decimal("5") # Ensure room for at least 5 levels
|
|
if grid_value < min_grid_value:
|
|
self.logger().info(
|
|
f"Grid value {grid_value} is too small for {trading_pair}. "
|
|
f"Minimum required for viable grid: {min_grid_value}"
|
|
)
|
|
return None # Skip grid creation if value is too small
|
|
|
|
# Select order frequency based on grid favorability
|
|
order_frequency = (
|
|
self.config.unfavorable_order_frequency if is_unfavorable
|
|
else self.config.favorable_order_frequency
|
|
)
|
|
# Calculate limit price to be more aggressive than grid boundaries
|
|
if side == TradeType.BUY:
|
|
# For buys, limit price should be lower than start price
|
|
limit_price = start_price * (1 - self.config.limit_price_spread)
|
|
else:
|
|
# For sells, limit price should be higher than end price
|
|
limit_price = end_price * (1 + self.config.limit_price_spread)
|
|
# Create the executor action
|
|
action = CreateExecutorAction(
|
|
controller_id=self.config.id,
|
|
executor_config=GridExecutorConfig(
|
|
timestamp=self.market_data_provider.time(),
|
|
connector_name=self.config.connector_name,
|
|
trading_pair=trading_pair,
|
|
side=side,
|
|
start_price=start_price,
|
|
end_price=end_price,
|
|
limit_price=limit_price,
|
|
leverage=self.config.leverage,
|
|
total_amount_quote=grid_value,
|
|
safe_extra_spread=self.config.safe_extra_spread,
|
|
min_spread_between_orders=self.config.min_spread_between_orders,
|
|
min_order_amount_quote=self.config.min_order_amount,
|
|
max_open_orders=self.config.max_open_orders,
|
|
order_frequency=order_frequency, # Use dynamic order frequency
|
|
max_orders_per_batch=self.config.max_orders_per_batch,
|
|
activation_bounds=self.config.activation_bounds,
|
|
keep_position=True, # Always keep position for potential reversal
|
|
coerce_tp_to_step=True,
|
|
triple_barrier_config=TripleBarrierConfig(
|
|
take_profit=self.config.grid_tp_multiplier,
|
|
open_order_type=OrderType.LIMIT_MAKER,
|
|
take_profit_order_type=OrderType.LIMIT_MAKER,
|
|
stop_loss=None,
|
|
time_limit=None,
|
|
trailing_stop=None,
|
|
)))
|
|
# Track unfavorable grid configs
|
|
if is_unfavorable:
|
|
self.unfavorable_grid_ids.add(action.executor_config.id)
|
|
self.logger().info(
|
|
f"Created unfavorable grid for {trading_pair} - "
|
|
f"Side: {side.name}, Value: ${grid_value:,.2f}, "
|
|
f"Order Frequency: {order_frequency}s"
|
|
)
|
|
else:
|
|
self.logger().info(
|
|
f"Created favorable grid for {trading_pair} - "
|
|
f"Side: {side.name}, Value: ${grid_value:,.2f}, "
|
|
f"Order Frequency: {order_frequency}s"
|
|
)
|
|
|
|
return action
|
|
|
|
def get_mid_price(self, trading_pair: str) -> Decimal:
|
|
return self.market_data_provider.get_price_by_type(self.config.connector_name, trading_pair, PriceType.MidPrice)
|