Skip to content

Commit

Permalink
Added only_rebalance_drifted_assets parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
brettelliot committed Jan 31, 2025
1 parent 7635a5a commit 23c45b6
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 6 deletions.
22 changes: 19 additions & 3 deletions lumibot/components/drift_rebalancer_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ class DriftRebalancerLogic:
When set to True, the strategy will only use fractional shares. The default is False, which means
the strategy will only use whole shares.
only_rebalance_drifted_assets : bool, optional
If True, the strategy will only rebalance assets whose drift exceeds the drift_threshold.
The default is False, which means the strategy will rebalance all assets in the portfolio.
"""

def __init__(
Expand All @@ -96,7 +100,8 @@ def __init__(
acceptable_slippage: Decimal = Decimal("0.005"),
fill_sleeptime: int = 15,
shorting: bool = False,
fractional_shares: bool = False
fractional_shares: bool = False,
only_rebalance_drifted_assets: bool = False
) -> None:
self.strategy = strategy
self.calculation_logic = DriftCalculationLogic(
Expand All @@ -111,7 +116,8 @@ def __init__(
acceptable_slippage=acceptable_slippage,
shorting=shorting,
order_type=order_type,
fractional_shares=fractional_shares
fractional_shares=fractional_shares,
only_rebalance_drifted_assets=only_rebalance_drifted_assets
)

def calculate(self, portfolio_weights: List[Dict[str, Any]]) -> pd.DataFrame:
Expand Down Expand Up @@ -313,7 +319,8 @@ def __init__(
acceptable_slippage: Decimal = Decimal("0.005"),
shorting: bool = False,
order_type: Order.OrderType = Order.OrderType.LIMIT,
fractional_shares: bool = False
fractional_shares: bool = False,
only_rebalance_drifted_assets: bool = False
) -> None:
self.strategy = strategy
self.drift_threshold = drift_threshold
Expand All @@ -322,6 +329,7 @@ def __init__(
self.shorting = shorting
self.order_type = order_type
self.fractional_shares = fractional_shares
self.only_rebalance_drifted_assets = only_rebalance_drifted_assets

# Sanity checks
if self.acceptable_slippage >= self.drift_threshold:
Expand Down Expand Up @@ -379,6 +387,10 @@ def _rebalance(self, df: pd.DataFrame = None) -> None:
sell_orders.append(order)

elif row["drift"] < 0:

if self.only_rebalance_drifted_assets and abs(row["drift"]) < self.drift_threshold:
continue

base_asset = row["base_asset"]
last_price = Decimal(self.strategy.get_last_price(base_asset))
limit_price = self.calculate_limit_price(last_price=last_price, side="sell")
Expand Down Expand Up @@ -426,6 +438,10 @@ def _rebalance(self, df: pd.DataFrame = None) -> None:
cash_position -= quantity * limit_price

elif row["drift"] > 0:

if self.only_rebalance_drifted_assets and abs(row["drift"]) < self.drift_threshold:
continue

base_asset = row["base_asset"]
last_price = Decimal(self.strategy.get_last_price(base_asset))
limit_price = self.calculate_limit_price(last_price=last_price, side="buy")
Expand Down
4 changes: 3 additions & 1 deletion lumibot/example_strategies/classic_60_40_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@
"weight": Decimal("0.4")
}
],
"shorting": False
"shorting": False,
"fractional_shares": False,
"only_rebalance_drifted_assets": False,
}
3 changes: 2 additions & 1 deletion lumibot/example_strategies/crypto_50_50.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"acceptable_slippage": "0.005", # 50 BPS
"fill_sleeptime": 15,
"shorting": False,
"fractional_shares": True
"fractional_shares": True,
"only_rebalance_drifted_assets": False,
}

if not is_live:
Expand Down
7 changes: 6 additions & 1 deletion lumibot/example_strategies/drift_rebalancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ class DriftRebalancer(Strategy):
"weight": Decimal("0.4")
}
],
"shorting": False
"shorting": False,
"fractional_shares": False,
"only_rebalance_drifted_assets": False,
}
Description of parameters:
Expand Down Expand Up @@ -79,6 +81,7 @@ class DriftRebalancer(Strategy):
- portfolio_weights: A list of dictionaries containing the base_asset and weight of each asset in the portfolio.
- shorting: If you want to allow shorting, set this to True. Default is False.
- fractional_shares: If you want to allow fractional shares, set this to True. Default is False.
- only_rebalance_drifted_assets: If you want to only rebalance assets that have drifted, set this to True. Default is False.
"""

Expand All @@ -94,6 +97,7 @@ def initialize(self, parameters: Any = None) -> None:
self.portfolio_weights = self.parameters.get("portfolio_weights", {})
self.shorting = self.parameters.get("shorting", False)
self.fractional_shares = self.parameters.get("fractional_shares", False)
self.only_rebalance_drifted_assets = self.parameters.get("only_rebalance_drifted_assets", False)
self.drift_df = pd.DataFrame()
self.drift_rebalancer_logic = DriftRebalancerLogic(
strategy=self,
Expand All @@ -104,6 +108,7 @@ def initialize(self, parameters: Any = None) -> None:
fill_sleeptime=self.fill_sleeptime,
shorting=self.shorting,
fractional_shares=self.fractional_shares,
only_rebalance_drifted_assets=self.only_rebalance_drifted_assets,
)

# noinspection PyAttributeOutsideInit
Expand Down
86 changes: 86 additions & 0 deletions tests/test_drift_rebalancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,49 @@ def test_selling_part_of_a_holding_with_limit_order(self):
assert strategy.orders[0].quantity == Decimal("5")
assert strategy.orders[0].type == Order.OrderType.LIMIT

def test_selling_with_only_rebalance_drifted_assets_when_over_drift_threshold(self):
strategy = MockStrategyWithOrderLogic(
broker=self.backtesting_broker,
order_type=Order.OrderType.LIMIT
)
df = pd.DataFrame({
"symbol": ["AAPL"],
"base_asset": [Asset("AAPL", "stock")],
"is_quote_asset": False,
"current_quantity": [Decimal("10")],
"current_value": [Decimal("1000")],
"current_weight": [Decimal("1.0")],
"target_weight": Decimal("0.5"),
"target_value": Decimal("500"),
"drift": Decimal("-0.5")
})
strategy.order_logic.only_rebalance_drifted_assets = True
strategy.order_logic.rebalance(drift_df=df)
assert len(strategy.orders) == 1
assert strategy.orders[0].side == "sell"
assert strategy.orders[0].quantity == Decimal("5")
assert strategy.orders[0].type == Order.OrderType.LIMIT

def test_selling_with_only_rebalance_drifted_assets_when_not_over_drift_threshold(self):
strategy = MockStrategyWithOrderLogic(
broker=self.backtesting_broker,
order_type=Order.OrderType.LIMIT
)
df = pd.DataFrame({
"symbol": ["AAPL"],
"base_asset": [Asset("AAPL", "stock")],
"is_quote_asset": False,
"current_quantity": [Decimal("10")],
"current_value": [Decimal("1000")],
"current_weight": [Decimal("1.0")],
"target_weight": Decimal("0.5"),
"target_value": Decimal("500"),
"drift": Decimal("-0.001") # make drift small
})
strategy.order_logic.only_rebalance_drifted_assets = True
strategy.order_logic.rebalance(drift_df=df)
assert len(strategy.orders) == 0

def test_selling_part_of_a_holding_with_market_order(self):
strategy = MockStrategyWithOrderLogic(
broker=self.backtesting_broker,
Expand Down Expand Up @@ -1319,6 +1362,49 @@ def test_selling_some_with_fractional_limit_orders(self):
assert strategy.orders[0].quantity == Decimal("1.507537688")
assert strategy.orders[0].type == Order.OrderType.LIMIT

def test_buying_with_only_rebalance_drifted_assets_when_over_drift_threshold(self):
strategy = MockStrategyWithOrderLogic(
broker=self.backtesting_broker,
order_type=Order.OrderType.LIMIT
)
df = pd.DataFrame({
"symbol": ["AAPL"],
"base_asset": [Asset("AAPL", "stock")],
"is_quote_asset": False,
"current_quantity": [Decimal("0")],
"current_value": [Decimal("0")],
"current_weight": [Decimal("0.0")],
"target_weight": Decimal("0.5"),
"target_value": Decimal("500"),
"drift": Decimal("0.5")
})
strategy.order_logic.only_rebalance_drifted_assets = True
strategy.order_logic.rebalance(drift_df=df)
assert len(strategy.orders) == 1
assert strategy.orders[0].side == "buy"
assert strategy.orders[0].quantity == Decimal("4.0") #
assert strategy.orders[0].type == Order.OrderType.LIMIT

def test_buying_with_only_rebalance_drifted_assets_when_not_over_drift_threshold(self):
strategy = MockStrategyWithOrderLogic(
broker=self.backtesting_broker,
order_type=Order.OrderType.LIMIT
)
df = pd.DataFrame({
"symbol": ["AAPL"],
"base_asset": [Asset("AAPL", "stock")],
"is_quote_asset": False,
"current_quantity": [Decimal("0")],
"current_value": [Decimal("0")],
"current_weight": [Decimal("0.0")],
"target_weight": Decimal("0.5"),
"target_value": Decimal("500"),
"drift": Decimal("0.001") # make drift small
})
strategy.order_logic.only_rebalance_drifted_assets = True
strategy.order_logic.rebalance(drift_df=df)
assert len(strategy.orders) == 0


# @pytest.mark.skip()
class TestDriftRebalancer:
Expand Down

0 comments on commit 23c45b6

Please sign in to comment.