Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added only_rebalance_drifted_assets parameter #694

Merged
merged 1 commit into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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