Skip to content
All library documents

Cointegrated-Pair Statistical Arbitrage With Hedged Execution

Article Strategy library · Author: hummingbot

Summary

This controller describes a two-asset statistical arbitrage workflow. It requests candle data for each pair, estimates a hedge relationship with linear regression, and uses the spread’s z-score to choose between long-dominant/short-hedge and the reverse. It allocates a target quote value across the legs and manages orders through paired position executors, with limits on order counts, position deviation, and quote refresh timing.

Execution management includes stopping obsolete orders, preserving filled positions after a cooldown, reducing exposure when the signal reverses, and closing positions when pair-level profit or loss limits are crossed. The configuration also supports perpetual-market position mode and leverage. The source is implementation-oriented rather than empirical: it provides no backtest results or evidence that the selected assets remain cointegrated. Its performance depends on the stability of the relationship, execution quality, and the risks of leg imbalance and leveraged positions.

Key ideas

  • The controller derives directional pair trades from a spread z-score and estimated hedge relationship.
  • It splits a target quote allocation between dominant and hedge assets according to a configured hedge ratio.
  • Order placement is constrained by per-leg order limits, target position gaps, and imbalance checks.
  • Signal reversals and pair-level profit or loss thresholds trigger position reduction.
  • The implementation describes execution mechanics but supplies no evidence that the pair remains cointegrated or profitable.

Tags

Full text
# StatArb


# StatArb









Configuration for a statistical arbitrage controller that trades two cointegrated assets.

## Source (Apache-2.0)

```python
from decimal import Decimal
from typing import List

import numpy as np
from sklearn.linear_model import LinearRegression

from hummingbot.core.data_type.common import OrderType, PositionAction, 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, PositionSummary
from hummingbot.strategy_v2.executors.order_executor.data_types import ExecutionStrategy, OrderExecutorConfig
from hummingbot.strategy_v2.executors.position_executor.data_types import PositionExecutorConfig, TripleBarrierConfig
from hummingbot.strategy_v2.models.executor_actions import CreateExecutorAction, ExecutorAction, StopExecutorAction


class StatArbConfig(ControllerConfigBase):
    """
    Configuration for a statistical arbitrage controller that trades two cointegrated assets.
    """
    controller_type: str = "generic"
    controller_name: str = "stat_arb"
    connector_pair_dominant: ConnectorPair = ConnectorPair(connector_name="binance_perpetual", trading_pair="SOL-USDT")
    connector_pair_hedge: ConnectorPair = ConnectorPair(connector_name="binance_perpetual", trading_pair="POPCAT-USDT")
    interval: str = "1m"
    lookback_period: int = 300
    entry_threshold: Decimal = Decimal("2.0")
    take_profit: Decimal = Decimal("0.0008")
    tp_global: Decimal = Decimal("0.01")
    sl_global: Decimal = Decimal("0.05")
    min_amount_quote: Decimal = Decimal("10")
    quoter_spread: Decimal = Decimal("0.0001")
    quoter_cooldown: int = 30
    quoter_refresh: int = 10
    max_orders_placed_per_side: int = 2
    max_orders_filled_per_side: int = 2
    max_position_deviation: Decimal = Decimal("0.1")
    pos_hedge_ratio: Decimal = Decimal("1.0")
    leverage: int = 20
    position_mode: PositionMode = PositionMode.HEDGE

    @property
    def triple_barrier_config(self) -> TripleBarrierConfig:
        return TripleBarrierConfig(
            take_profit=self.take_profit,
            open_order_type=OrderType.LIMIT_MAKER,
            take_profit_order_type=OrderType.LIMIT_MAKER,
        )

    def update_markets(self, markets: dict) -> dict:
        """Update markets dictionary with both trading pairs"""
        # Add dominant pair
        if self.connector_pair_dominant.connector_name not in markets:
            markets[self.connector_pair_dominant.connector_name] = set()
        markets[self.connector_pair_dominant.connector_name].add(self.connector_pair_dominant.trading_pair)

        # Add hedge pair
        if self.connector_pair_hedge.connector_name not in markets:
            markets[self.connector_pair_hedge.connector_name] = set()
        markets[self.connector_pair_hedge.connector_name].add(self.connector_pair_hedge.trading_pair)

        return markets


class StatArb(ControllerBase):
    """
    Statistical arbitrage controller that trades two cointegrated assets.
    """

    def __init__(self, config: StatArbConfig, *args, **kwargs):
        super().__init__(config, *args, **kwargs)
        self.config = config
        self.theoretical_dominant_quote = self.config.total_amount_quote * (1 / (1 + self.config.pos_hedge_ratio))
        self.theoretical_hedge_quote = self.config.total_amount_quote * (self.config.pos_hedge_ratio / (1 + self.config.pos_hedge_ratio))

        # Initialize processed data dictionary
        self.processed_data = {
            "dominant_price": None,
            "hedge_price": None,
            "spread": None,
            "z_score": None,
            "hedge_ratio": None,
            "position_dominant": Decimal("0"),
            "position_hedge": Decimal("0"),
            "active_orders_dominant": [],
            "active_orders_hedge": [],
            "pair_pnl": Decimal("0"),
            "signal": 0  # 0: no signal, 1: long dominant/short hedge, -1: short dominant/long hedge
        }

        # Setup max records for safety
        max_records = self.config.lookback_period + 20
        self.max_records = max_records
        if "_perpetual" in self.config.connector_pair_dominant.connector_name:
            connector = self.market_data_provider.get_connector(self.config.connector_pair_dominant.connector_name)
            connector.set_position_mode(self.config.position_mode)
            connector.set_leverage(self.config.connector_pair_dominant.trading_pair, self.config.leverage)
        if "_perpetual" in self.config.connector_pair_hedge.connector_name:
            connector = self.market_data_provider.get_connector(self.config.connector_pair_hedge.connector_name)
            connector.set_position_mode(self.config.position_mode)
            connector.set_leverage(self.config.connector_pair_hedge.trading_pair, self.config.leverage)

    def determine_executor_actions(self) -> List[ExecutorAction]:
        """
        The execution logic for the statistical arbitrage strategy.
        Market Data Conditions: Signal is generated based on the z-score of the spread between the two assets.
                                If signal == 1 --> long dominant/short hedge
                                If signal == -1 --> short dominant/long hedge
        Execution Conditions: If the signal is generated add position executors to quote from the dominant and hedge markets.
                              We compare the current position with the theoretical position for the dominant and hedge assets.
                              If the current position + the active placed amount is greater than the theoretical position, can't place more orders.
                              If the imbalance scaled pct is greater than the threshold, we avoid placing orders in the market passed on filtered_connector_pair.
                              If the pnl of total position is greater than the take profit or lower than the stop loss, we close the position.
        """
        actions: List[ExecutorAction] = []
        # Check global take profit and stop loss
        if self.processed_data["pair_pnl_pct"] > self.config.tp_global or self.processed_data["pair_pnl_pct"] < -self.config.sl_global:
            # Close all positions
            for position in self.positions_held:
                actions.extend(self.get_executors_to_reduce_position(position))
            return actions
        # Check the signal
        elif self.processed_data["signal"] != 0:
            actions.extend(self.get_executors_to_quote())
            actions.extend(self.get_executors_to_reduce_position_on_opposite_signal())

        # Get the executors to keep position after a cooldown is reached
        actions.extend(self.get_executors_to_keep_position())
        actions.extend(self.get_executors_to_refresh())

        return actions

    def get_executors_to_reduce_position_on_opposite_signal(self) -> List[ExecutorAction]:
        if self.processed_data["signal"] == 1:
            dominant_side, hedge_side = TradeType.SELL, TradeType.BUY
        elif self.processed_data["signal"] == -1:
            dominant_side, hedge_side = TradeType.BUY, TradeType.SELL
        else:
            return []
        # Get executors to stop
        dominant_active_executors_to_stop = self.filter_executors(self.executors_info, filter_func=lambda e: e.connector_name == self.config.connector_pair_dominant.connector_name and e.trading_pair == self.config.connector_pair_dominant.trading_pair and e.side == dominant_side)
        hedge_active_executors_to_stop = self.filter_executors(self.executors_info, filter_func=lambda e: e.connector_name == self.config.connector_pair_hedge.connector_name and e.trading_pair == self.config.connector_pair_hedge.trading_pair and e.side == hedge_side)
        stop_actions = [StopExecutorAction(controller_id=self.config.id, executor_id=executor.id, keep_position=False) for executor in dominant_active_executors_to_stop + hedge_active_executors_to_stop]

        # Get order executors to reduce positions
        reduce_actions: List[ExecutorAction] = []
        for position in self.positions_held:
            if position.connector_name == self.config.connector_pair_dominant.connector_name and position.trading_pair == self.config.connector_pair_dominant.trading_pair and position.side == dominant_side:
                reduce_actions.extend(self.get_executors_to_reduce_position(position))
            elif position.connector_name == self.config.connector_pair_hedge.connector_name and position.trading_pair == self.config.connector_pair_hedge.trading_pair and position.side == hedge_side:
                reduce_actions.extend(self.get_executors_to_reduce_position(position))
        return stop_actions + reduce_actions

    def get_executors_to_keep_position(self) -> List[ExecutorAction]:
        stop_actions: List[ExecutorAction] = []
        for executor in self.processed_data["executors_dominant_filled"] + self.processed_data["executors_hedge_filled"]:
            if self.market_data_provider.time() - executor.timestamp >= self.config.quoter_cooldown:
                # Create a new executor to keep the position
                stop_actions.append(StopExecutorAction(controller_id=self.config.id, executor_id=executor.id, keep_position=True))
        return stop_actions

    def get_executors_to_refresh(self) -> List[ExecutorAction]:
        refresh_actions: List[ExecutorAction] = []
        for executor in self.processed_data["executors_dominant_placed"] + self.processed_data["executors_hedge_placed"]:
            if self.market_data_provider.time() - executor.timestamp >= self.config.quoter_refresh:
                # Create a new executor to refresh the position
                refresh_actions.append(StopExecutorAction(controller_id=self.config.id, executor_id=executor.id, keep_position=False))
        return refresh_actions

    def get_executors_to_quote(self) -> List[ExecutorAction]:
        """
        Get Order Executor to quote from the dominant and hedge markets.
        """
        actions: List[ExecutorAction] = []
        trade_type_dominant = TradeType.BUY if self.processed_data["signal"] == 1 else TradeType.SELL
        trade_type_hedge = TradeType.SELL if self.processed_data["signal"] == 1 else TradeType.BUY

        # Analyze dominant active orders, max deviation and imbalance to create a new executor
        if self.processed_data["dominant_gap"] > Decimal("0") and \
           self.processed_data["filter_connector_pair"] != self.config.connector_pair_dominant and \
           len(self.processed_data["executors_dominant_placed"]) < self.config.max_orders_placed_per_side and \
           len(self.processed_data["executors_dominant_filled"]) < self.config.max_orders_filled_per_side:
            # Create Position Executor for dominant asset
            if trade_type_dominant == TradeType.BUY:
                price = self.processed_data["min_price_dominant"] * (1 - self.config.quoter_spread)
            else:
                price = self.processed_data["max_price_dominant"] * (1 + self.config.quoter_spread)
            dominant_executor_config = PositionExecutorConfig(
                timestamp=self.market_data_provider.time(),
                connector_name=self.config.connector_pair_dominant.connector_name,
                trading_pair=self.config.connector_pair_dominant.trading_pair,
                side=trade_type_dominant,
                entry_price=price,
                amount=self.config.min_amount_quote / self.processed_data["dominant_price"],
                triple_barrier_config=self.config.triple_barrier_config,
                leverage=self.config.leverage,
            )
            actions.append(CreateExecutorAction(controller_id=self.config.id, executor_config=dominant_executor_config))

        # Analyze hedge active orders, max deviation and imbalance to create a new executor
        if self.processed_data["hedge_gap"] > Decimal("0") and \
           self.processed_data["filter_connector_pair"] != self.config.connector_pair_hedge and \
           len(self.processed_data["executors_hedge_placed"]) < self.config.max_orders_placed_per_side and \
           len(self.processed_data["executors_hedge_filled"]) < self.config.max_orders_filled_per_side:
            # Create Position Executor for hedge asset
            if trade_type_hedge == TradeType.BUY:
                price = self.processed_data["min_price_hedge"] * (1 - self.config.quoter_spread)
            else:
                price = self.processed_data["max_price_hedge"] * (1 + self.config.quoter_spread)
            hedge_executor_config = PositionExecutorConfig(
                timestamp=self.market_data_provider.time(),
                connector_name=self.config.connector_pair_hedge.connector_name,
                trading_pair=self.config.connector_pair_hedge.trading_pair,
                side=trade_type_hedge,
                entry_price=price,
                amount=self.config.min_amount_quote / self.processed_data["hedge_price"],
                triple_barrier_config=self.config.triple_barrier_config,
                leverage=self.config.leverage,
            )
            actions.append(CreateExecutorAction(controller_id=self.config.id, executor_config=hedge_executor_config))
        return actions

    def get_executors_to_reduce_position(self, position: PositionSummary) -> List[ExecutorAction]:
        """
        Get Order Executor to reduce position.
        """
        if position.amount > Decimal("0"):
            # Close position
            config = OrderExecutorConfig(
                timestamp=self.market_data_provider.time(),
                connector_name=position.connector_name,
                trading_pair=position.trading_pair,
                side=TradeType.BUY if position.side == TradeType.SELL else TradeType.SELL,
                amount=position.amount,
                position_action=PositionAction.CLOSE,
                execution_strategy=ExecutionStrategy.MARKET,
                leverage=self.config.leverage,
            )
            return [CreateExecutorAction(controller_id=self.config.id, executor_config=config)]
        return []

    async def update_processed_data(self):
        """
        Update processed data with the latest market information and statistical calculations
        needed for the statistical arbitrage strategy.
        """
        # Stat arb analysis
        spread, z_score = self.get_spread_and_z_score()

        # Generate trading signal based on z-score
        entry_threshold = float(self.config.entry_threshold)
        if z_score > entry_threshold:
            # Spread is too high, expect it to revert: long dominant, short hedge
            signal = 1
            dominant_side, hedge_side = TradeType.BUY, TradeType.SELL
        elif z_score < -entry_threshold:
            # Spread is too low, expect it to revert: short dominant, long hedge
            signal = -1
            dominant_side, hedge_side = TradeType.SELL, TradeType.BUY
        else:
            # No signal
            signal = 0
            dominant_side, hedge_side = None, None

        # Current prices
        dominant_price, hedge_price = self.get_pairs_prices()

        # Get current positions stats by signal
        positions_dominant = next((position for position in self.positions_held if position.connector_name == self.config.connector_pair_dominant.connector_name and position.trading_pair == self.config.connector_pair_dominant.trading_pair and (position.side == dominant_side or dominant_side is None)), None)
        positions_hedge = next((position for position in self.positions_held if position.connector_name == self.config.connector_pair_hedge.connector_name and position.trading_pair == self.config.connector_pair_hedge.trading_pair and (position.side == hedge_side or hedge_side is None)), None)
        # Get position stats
        position_dominant_quote = positions_dominant.amount_quote if positions_dominant else Decimal("0")
        position_hedge_quote = positions_hedge.amount_quote if positions_hedge else Decimal("0")
        position_dominant_pnl_quote = positions_dominant.global_pnl_quote if positions_dominant else Decimal("0")
        position_hedge_pnl_quote = positions_hedge.global_pnl_quote if positions_hedge else Decimal("0")
        pair_pnl_pct = (position_dominant_pnl_quote + position_hedge_pnl_quote) / (position_dominant_quote + position_hedge_quote) if (position_dominant_quote + position_hedge_quote) != 0 else Decimal("0")
        # Get active executors
        executors_dominant_placed, executors_dominant_filled = self.get_executors_dominant()
        executors_hedge_placed, executors_hedge_filled = self.get_executors_hedge()
        min_price_dominant = Decimal(str(min([executor.config.entry_price for executor in executors_dominant_placed]))) if executors_dominant_placed else None
        max_price_dominant = Decimal(str(max([executor.config.entry_price for executor in executors_dominant_placed]))) if executors_dominant_placed else None
        min_price_hedge = Decimal(str(min([executor.config.entry_price for executor in executors_hedge_placed]))) if executors_hedge_placed else None
        max_price_hedge = Decimal(str(max([executor.config.entry_price for executor in executors_hedge_placed]))) if executors_hedge_placed else None

        active_amount_dominant = Decimal(str(sum([executor.filled_amount_quote for executor in executors_dominant_filled])))
        active_amount_hedge = Decimal(str(sum([executor.filled_amount_quote for executor in executors_hedge_filled])))

        # Compute imbalance based on the hedge ratio
        dominant_gap = self.theoretical_dominant_quote - position_dominant_quote - active_amount_dominant
        hedge_gap = self.theoretical_hedge_quote - position_hedge_quote - active_amount_hedge
        imbalance = position_dominant_quote - position_hedge_quote
        imbalance_scaled = position_dominant_quote - position_hedge_quote * self.config.pos_hedge_ratio
        imbalance_scaled_pct = imbalance_scaled / position_dominant_quote if position_dominant_quote != Decimal("0") else Decimal("0")
        filter_connector_pair = None
        if imbalance_scaled_pct > self.config.max_position_deviation:
            # Avoid placing orders in the dominant market
            filter_connector_pair = self.config.connector_pair_dominant
        elif imbalance_scaled_pct < -self.config.max_position_deviation:
            # Avoid placing orders in the hedge market
            filter_connector_pair = self.config.connector_pair_hedge

        # Update processed data
        self.processed_data.update({
            "dominant_price": Decimal(str(dominant_price)),
            "hedge_price": Decimal(str(hedge_price)),
            "spread": Decimal(str(spread)),
            "z_score": Decimal(str(z_score)),
            "dominant_gap": Decimal(str(dominant_gap)),
            "hedge_gap": Decimal(str(hedge_gap)),
            "position_dominant_quote": position_dominant_quote,
            "position_hedge_quote": position_hedge_quote,
            "active_amount_dominant": active_amount_dominant,
            "active_amount_hedge": active_amount_hedge,
            "signal": signal,
            # Store full dataframes for reference
            "imbalance": Decimal(str(imbalance)),
            "imbalance_scaled_pct": Decimal(str(imbalance_scaled_pct)),
            "filter_connector_pair": filter_connector_pair,
            "min_price_dominant": min_price_dominant if min_price_dominant is not None else Decimal(str(dominant_price)),
            "max_price_dominant": max_price_dominant if max_price_dominant is not None else Decimal(str(dominant_price)),
            "min_price_hedge": min_price_hedge if min_price_hedge is not None else Decimal(str(hedge_price)),
            "max_price_hedge": max_price_hedge if max_price_hedge is not None else Decimal(str(hedge_price)),
            "executors_dominant_filled": executors_dominant_filled,
            "executors_hedge_filled": executors_hedge_filled,
            "executors_dominant_placed": executors_dominant_placed,
            "executors_hedge_placed": executors_hedge_placed,
            "pair_pnl_pct": pair_pnl_pct,
        })

    def get_spread_and_z_score(self):
        # Fetch candle data for both assets
        dominant_df = self.market_data_provider.get_candles_df(
            connector_name=self.config.connector_pair_dominant.connector_name,
            trading_pair=self.config.connector_pair_dominant.trading_pair,
            interval=self.config.interval,
            max_records=self.max_records
        )

        hedge_df = self.market_data_provider.get_candles_df(
            connector_name=self.config.connector_pair_hedge.connector_name,
            trading_pair=self.config.connector_pair_hedge.trading_pair,
            interval=self.config.interval,
            max_records=self.max_records
        )

        if dominant_df.empty or hedge_df.empty:
            self.logger().warning("Not enough candle data available for statistical analysis")
            return

        # Extract close prices
        dominant_prices = dominant_df['close'].values
        hedge_prices = hedge_df['close'].values

        # Ensure we have enough data and both series have the same length
        min_length = min(len(dominant_prices), len(hedge_prices))
        if min_length < self.config.lookback_period:
            self.logger().warning(
                f"Not enough data points for analysis. Required: {self.config.lookback_period}, Available: {min_length}")
            return

        # Use the most recent data points
        dominant_prices = dominant_prices[-self.config.lookback_period:]
        hedge_prices = hedge_prices[-self.config.lookback_period:]

        # Convert to numpy arrays
        dominant_prices_np = np.array(dominant_prices, dtype=float)
        hedge_prices_np = np.array(hedge_prices, dtype=float)

        # Calculate percentage returns
        dominant_pct_change = np.diff(dominant_prices_np) / dominant_prices_np[:-1]
        hedge_pct_change = np.diff(hedge_prices_np) / hedge_prices_np[:-1]

        # Convert to cumulative returns
        dominant_cum_returns = np.cumprod(dominant_pct_change + 1)
        hedge_cum_returns = np.cumprod(hedge_pct_change + 1)

        # Normalize to start at 1
        dominant_cum_returns = dominant_cum_returns / dominant_cum_returns[0] if len(dominant_cum_returns) > 0 else np.array([1.0])
        hedge_cum_returns = hedge_cum_returns / hedge_cum_returns[0] if len(hedge_cum_returns) > 0 else np.array([1.0])

        # Perform linear regression
        dominant_cum_returns_reshaped = dominant_cum_returns.reshape(-1, 1)
        reg = LinearRegression().fit(dominant_cum_returns_reshaped, hedge_cum_returns)
        alpha = reg.intercept_
        beta = reg.coef_[0]
        self.processed_data.update({
            "alpha": alpha,
            "beta": beta,
        })

        # Calculate spread as percentage difference from predicted value
        y_pred = alpha + beta * dominant_cum_returns
        spread_pct = (hedge_cum_returns - y_pred) / y_pred * 100

        # Calculate z-score
        mean_spread = np.mean(spread_pct)
        std_spread = np.std(spread_pct)
        if std_spread == 0:
            self.logger().warning("Standard deviation of spread is zero, cannot calculate z-score")
            return

        current_spread = spread_pct[-1]
        current_z_score = (current_spread - mean_spread) / std_spread

        return current_spread, current_z_score

    def get_pairs_prices(self):
        current_dominant_price = self.market_data_provider.get_price_by_type(
            connector_name=self.config.connector_pair_dominant.connector_name,
            trading_pair=self.config.connector_pair_dominant.trading_pair, price_type=PriceType.MidPrice)

        current_hedge_price = self.market_data_provider.get_price_by_type(
            connector_name=self.config.connector_pair_hedge.connector_name,
            trading_pair=self.config.connector_pair_hedge.trading_pair, price_type=PriceType.MidPrice)
        return current_dominant_price, current_hedge_price

    def get_executors_dominant(self):
        active_executors_dominant_placed = self.filter_executors(
            self.executors_info,
            filter_func=lambda e: e.connector_name == self.config.connector_pair_dominant.connector_name and e.trading_pair == self.config.connector_pair_dominant.trading_pair and e.is_active and not e.is_trading and e.type == "position_executor"
        )
        active_executors_dominant_filled = self.filter_executors(
            self.executors_info,
            filter_func=lambda e: e.connector_name == self.config.connector_pair_dominant.connector_name and e.trading_pair == self.config.connector_pair_dominant.trading_pair and e.is_active and e.is_trading and e.type == "position_executor"
        )
        return active_executors_dominant_placed, active_executors_dominant_filled

    def get_executors_hedge(self):
        active_executors_hedge_placed = self.filter_executors(
            self.executors_info,
            filter_func=lambda e: e.connector_name == self.config.connector_pair_hedge.connector_name and e.trading_pair == self.config.connector_pair_hedge.trading_pair and e.is_active and not e.is_trading and e.type == "position_executor"
        )
        active_executors_hedge_filled = self.filter_executors(
            self.executors_info,
            filter_func=lambda e: e.connector_name == self.config.connector_pair_hedge.connector_name and e.trading_pair == self.config.connector_pair_hedge.trading_pair and e.is_active and e.is_trading and e.type == "position_executor"
        )
        return active_executors_hedge_placed, active_executors_hedge_filled

    def to_format_status(self) -> List[str]:
        """
        Format the status of the controller for display.
        """
        status_lines = []
        status_lines.append(f"""
Dominant Pair: {self.config.connector_pair_dominant} | Hedge Pair: {self.config.connector_pair_hedge} |
Timeframe: {self.config.interval} | Lookback Period: {self.config.lookback_period} | Entry Threshold: {self.config.entry_threshold}

Positions targets:
Theoretical Dominant         : {self.theoretical_dominant_quote} | Theoretical Hedge: {self.theoretical_hedge_quote} | Position Hedge Ratio: {self.config.pos_hedge_ratio}
Position Dominant            : {self.processed_data['position_dominant_quote']:.2f} | Position Hedge: {self.processed_data['position_hedge_quote']:.2f} | Imbalance: {self.processed_data['imbalance']:.2f} | Imbalance Scaled: {self.processed_data['imbalance_scaled_pct']:.2f} %

Current Executors:
Active Orders Dominant       : {len(self.processed_data['executors_dominant_placed'])} | Active Orders Hedge       : {len(self.processed_data['executors_hedge_placed'])} |
Active Orders Dominant Filled: {len(self.processed_data['executors_dominant_filled'])} | Active Orders Hedge Filled: {len(self.processed_data['executors_hedge_filled'])}

Signal: {self.processed_data['signal']:.2f} | Z-Score: {self.processed_data['z_score']:.2f} | Spread: {self.processed_data['spread']:.2f}
Alpha : {self.processed_data['alpha']:.2f} | Beta: {self.processed_data['beta']:.2f}
Pair PnL PCT: {self.processed_data['pair_pnl_pct'] * 100:.2f} %
""")
        return status_lines

    def get_candles_config(self) -> List[CandlesConfig]:
        max_records = self.config.lookback_period + 20
        return [
            CandlesConfig(
                connector=self.config.connector_pair_dominant.connector_name,
                trading_pair=self.config.connector_pair_dominant.trading_pair,
                interval=self.config.interval,
                max_records=max_records
            ),
            CandlesConfig(
                connector=self.config.connector_pair_hedge.connector_name,
                trading_pair=self.config.connector_pair_hedge.trading_pair,
                interval=self.config.interval,
                max_records=max_records
            )
        ]

```

Shown in full with attribution under the source's licence. Licence: Apache-2.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.