From dec5060cab9fdc513cc2e250d1f17cfee58a64b8 Mon Sep 17 00:00:00 2001 From: Brett Elliot Date: Mon, 4 Nov 2024 06:02:56 -0500 Subject: [PATCH 1/8] readme update --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 69bfc2848..68ffed417 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,6 @@ git fetch origin git merge origin/dev git checkout my-feature git rebase dev -git checkout my-feature git push --force-with-lease origin my-feature ``` From d9442bd1cf673dfc09fb2d4549566a36df6de0ca Mon Sep 17 00:00:00 2001 From: Brett Elliot Date: Mon, 4 Nov 2024 10:51:44 -0500 Subject: [PATCH 2/8] fix bug so that negative absolute drifts cause a rebalance --- lumibot/strategies/drift_rebalancer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lumibot/strategies/drift_rebalancer.py b/lumibot/strategies/drift_rebalancer.py index b42cd3c3f..fa2da4fa7 100644 --- a/lumibot/strategies/drift_rebalancer.py +++ b/lumibot/strategies/drift_rebalancer.py @@ -123,10 +123,10 @@ def on_trading_iteration(self) -> None: # Check if the absolute value of any drift is greater than the threshold rebalance_needed = False for index, row in self.drift_df.iterrows(): - if row["absolute_drift"] > self.absolute_drift_threshold: + if abs(row["absolute_drift"]) > self.absolute_drift_threshold: rebalance_needed = True msg = ( - f"Absolute drift for {row['symbol']} is {row['absolute_drift']:.2f} " + f"Absolute drift for {row['symbol']} is {abs(row['absolute_drift']):.2f} " f"and exceeds threshold of {self.absolute_drift_threshold:.2f}" ) logger.info(msg) From c0fcd7b932886f0bf88a664b69df3521f83060d5 Mon Sep 17 00:00:00 2001 From: Brett Elliot Date: Mon, 4 Nov 2024 20:59:42 -0500 Subject: [PATCH 3/8] fix bug in calculating limit price; set default slippate to 50 BPS; attempt to get status of broker orders during trading iteration --- lumibot/example_strategies/classic_60_40.py | 2 +- lumibot/strategies/drift_rebalancer.py | 39 +++++++++++++++------ tests/test_drift_rebalancer.py | 37 ++++++++++++++++--- 3 files changed, 62 insertions(+), 16 deletions(-) diff --git a/lumibot/example_strategies/classic_60_40.py b/lumibot/example_strategies/classic_60_40.py index dd329940e..8f9301f20 100644 --- a/lumibot/example_strategies/classic_60_40.py +++ b/lumibot/example_strategies/classic_60_40.py @@ -18,7 +18,7 @@ "market": "NYSE", "sleeptime": "1D", "absolute_drift_threshold": "0.15", - "acceptable_slippage": "0.0005", + "acceptable_slippage": "0.005", # 50 BPS "fill_sleeptime": 15, "target_weights": { "SPY": "0.60", diff --git a/lumibot/strategies/drift_rebalancer.py b/lumibot/strategies/drift_rebalancer.py index fa2da4fa7..0da4b5cda 100644 --- a/lumibot/strategies/drift_rebalancer.py +++ b/lumibot/strategies/drift_rebalancer.py @@ -52,8 +52,8 @@ class DriftRebalancer(Strategy): "absolute_drift_threshold": "0.05", # This is the acceptable slippage that will be used when calculating the number of shares to buy or sell. - # The default is 0.0005 (5 BPS) - "acceptable_slippage": "0.0005", + # The default is 0.005 (50 BPS) + "acceptable_slippage": "0.005", # 50 BPS # The amount of time to sleep between the sells and buys to give enough time for the orders to fill "fill_sleeptime": 15, @@ -72,7 +72,7 @@ def initialize(self, parameters: Any = None) -> None: self.set_market(self.parameters.get("market", "NYSE")) self.sleeptime = self.parameters.get("sleeptime", "1D") self.absolute_drift_threshold = Decimal(self.parameters.get("absolute_drift_threshold", "0.20")) - self.acceptable_slippage = Decimal(self.parameters.get("acceptable_slippage", "0.0005")) + self.acceptable_slippage = Decimal(self.parameters.get("acceptable_slippage", "0.005")) self.fill_sleeptime = self.parameters.get("fill_sleeptime", 15) self.target_weights = {k: Decimal(v) for k, v in self.parameters["target_weights"].items()} self.drift_df = pd.DataFrame() @@ -223,7 +223,7 @@ def __init__( strategy: Strategy, df: pd.DataFrame, fill_sleeptime: int = 15, - acceptable_slippage: Decimal = Decimal("0.0005") + acceptable_slippage: Decimal = Decimal("0.005") ) -> None: self.strategy = strategy self.df = df @@ -232,6 +232,7 @@ def __init__( def rebalance(self) -> None: # Execute sells first + sell_orders = [] for index, row in self.df.iterrows(): if row["absolute_drift"] == -1: # Sell everything @@ -239,18 +240,36 @@ def rebalance(self) -> None: quantity = row["current_quantity"] last_price = Decimal(self.strategy.get_last_price(symbol)) limit_price = self.calculate_limit_price(last_price=last_price, side="sell") - self.place_limit_order(symbol=symbol, quantity=quantity, limit_price=limit_price, side="sell") + order = self.place_limit_order( + symbol=symbol, + quantity=quantity, + limit_price=limit_price, + side="sell" + ) + sell_orders.append(order) elif row["absolute_drift"] < 0: symbol = row["symbol"] last_price = Decimal(self.strategy.get_last_price(symbol)) limit_price = self.calculate_limit_price(last_price=last_price, side="sell") quantity = ((row["current_value"] - row["target_value"]) / limit_price).quantize(Decimal('1'), rounding=ROUND_DOWN) if quantity > 0: - self.place_limit_order(symbol=symbol, quantity=quantity, limit_price=limit_price, side="sell") + order = self.place_limit_order( + symbol=symbol, + quantity=quantity, + limit_price=limit_price, + side="sell" + ) + sell_orders.append(order) + + for order in sell_orders: + logger.info(f"Submitted sell order: {order}") if not self.strategy.is_backtesting: # Sleep to allow sell orders to fill time.sleep(self.fill_sleeptime) + orders = self.strategy.broker._pull_all_orders(self.strategy.name, self.strategy) + for order in orders: + logger.info(f"Order at broker: {order}") # Get current cash position from the broker cash_position = self.get_current_cash_position() @@ -271,19 +290,19 @@ def rebalance(self) -> None: def calculate_limit_price(self, *, last_price: Decimal, side: str) -> Decimal: if side == "sell": - return last_price * (1 - self.acceptable_slippage / Decimal(10000)) + return last_price * (1 - self.acceptable_slippage) elif side == "buy": - return last_price * (1 + self.acceptable_slippage / Decimal(10000)) + return last_price * (1 + self.acceptable_slippage) def get_current_cash_position(self) -> Decimal: self.strategy.update_broker_balances(force_update=True) return Decimal(self.strategy.cash) - def place_limit_order(self, *, symbol: str, quantity: Decimal, limit_price: Decimal, side: str) -> None: + def place_limit_order(self, *, symbol: str, quantity: Decimal, limit_price: Decimal, side: str) -> Any: limit_order = self.strategy.create_order( asset=symbol, quantity=quantity, side=side, limit_price=float(limit_price) ) - self.strategy.submit_order(limit_order) + return self.strategy.submit_order(limit_order) diff --git a/tests/test_drift_rebalancer.py b/tests/test_drift_rebalancer.py index b8e6b7677..e295cf2cd 100644 --- a/tests/test_drift_rebalancer.py +++ b/tests/test_drift_rebalancer.py @@ -319,6 +319,7 @@ def update_broker_balances(self, force_update: bool = False) -> None: def submit_order(self, order) -> None: self.orders.append(order) + return order class TestLimitOrderRebalance: @@ -418,6 +419,32 @@ def test_attempting_to_sell_when_the_amount_we_need_to_sell_is_less_than_the_lim executor.rebalance() assert len(strategy.orders) == 0 + def test_calculate_limit_price_when_selling(self): + strategy = MockStrategy(broker=self.backtesting_broker) + df = pd.DataFrame({ + "symbol": ["AAPL"], + "current_quantity": [Decimal("10")], + "current_value": [Decimal("1000")], + "target_value": [Decimal("0")], + "absolute_drift": [Decimal("-1")] + }) + executor = LimitOrderRebalanceLogic(strategy=strategy, df=df, acceptable_slippage=Decimal("0.005")) + limit_price = executor.calculate_limit_price(last_price=Decimal("120.00"), side="sell") + assert limit_price == Decimal("119.4") + + def test_calculate_limit_price_when_buying(self): + strategy = MockStrategy(broker=self.backtesting_broker) + df = pd.DataFrame({ + "symbol": ["AAPL"], + "current_quantity": [Decimal("10")], + "current_value": [Decimal("1000")], + "target_value": [Decimal("0")], + "absolute_drift": [Decimal("-1")] + }) + executor = LimitOrderRebalanceLogic(strategy=strategy, df=df, acceptable_slippage=Decimal("0.005")) + limit_price = executor.calculate_limit_price(last_price=Decimal("120.00"), side="buy") + assert limit_price == Decimal("120.6") + # @pytest.mark.skip() class TestDriftRebalancer: @@ -432,7 +459,7 @@ def test_classic_60_60(self, pandas_data_fixture): "market": "NYSE", "sleeptime": "1D", "absolute_drift_threshold": "0.03", - "acceptable_slippage": "0.0005", + "acceptable_slippage": "0.005", "fill_sleeptime": 15, "target_weights": { "SPY": "0.60", @@ -456,7 +483,7 @@ def test_classic_60_60(self, pandas_data_fixture): ) assert results is not None - assert np.isclose(results["cagr"], 0.22310804893738934, atol=1e-4) - assert np.isclose(results["volatility"], 0.0690583452535692, atol=1e-4) - assert np.isclose(results["sharpe"], 3.0127864810707985, atol=1e-4) - assert np.isclose(results["max_drawdown"]["drawdown"], 0.025983871768394628, atol=1e-4) + assert np.isclose(results["cagr"], 0.22076538945204272, atol=1e-4) + assert np.isclose(results["volatility"], 0.06740737779031068, atol=1e-4) + assert np.isclose(results["sharpe"], 3.051823053251843, atol=1e-4) + assert np.isclose(results["max_drawdown"]["drawdown"], 0.025697778711759052, atol=1e-4) From 0666d79c6e338a867b145d6236300b81e722ea96 Mon Sep 17 00:00:00 2001 From: Brett Elliot Date: Tue, 5 Nov 2024 06:19:26 -0500 Subject: [PATCH 4/8] rename absolute_drift to just drift --- lumibot/example_strategies/classic_60_40.py | 2 +- lumibot/strategies/drift_rebalancer.py | 68 +++++++++++---------- tests/test_drift_rebalancer.py | 43 ++++++++----- 3 files changed, 66 insertions(+), 47 deletions(-) diff --git a/lumibot/example_strategies/classic_60_40.py b/lumibot/example_strategies/classic_60_40.py index 8f9301f20..14ad3a199 100644 --- a/lumibot/example_strategies/classic_60_40.py +++ b/lumibot/example_strategies/classic_60_40.py @@ -17,7 +17,7 @@ parameters = { "market": "NYSE", "sleeptime": "1D", - "absolute_drift_threshold": "0.15", + "drift_threshold": "0.15", "acceptable_slippage": "0.005", # 50 BPS "fill_sleeptime": 15, "target_weights": { diff --git a/lumibot/strategies/drift_rebalancer.py b/lumibot/strategies/drift_rebalancer.py index 0da4b5cda..3dcc40905 100644 --- a/lumibot/strategies/drift_rebalancer.py +++ b/lumibot/strategies/drift_rebalancer.py @@ -30,7 +30,7 @@ class DriftRebalancer(Strategy): """The DriftRebalancer strategy rebalances a portfolio based on drift from target weights. The strategy calculates the drift of each asset in the portfolio and triggers a rebalance if the drift exceeds - the absolute_drift_threshold. The strategy will sell assets that have drifted above the threshold and + the drift_threshold. The strategy will sell assets that have drifted above the threshold and buy assets that have drifted below the threshold. The current version of the DriftRebalancer strategy only supports limit orders and whole share quantities. @@ -46,10 +46,10 @@ class DriftRebalancer(Strategy): ### DriftRebalancer parameters - # This is the absolute drift threshold that will trigger a rebalance. If the target_weight is 0.30 and the - # absolute_drift_threshold is 0.05, then the rebalance will be triggered when the assets current_weight + # This is the drift threshold that will trigger a rebalance. If the target_weight is 0.30 and the + # drift_threshold is 0.05, then the rebalance will be triggered when the assets current_weight # is less than 0.25 or greater than 0.35. - "absolute_drift_threshold": "0.05", + "drift_threshold": "0.05", # This is the acceptable slippage that will be used when calculating the number of shares to buy or sell. # The default is 0.005 (50 BPS) @@ -71,28 +71,30 @@ class DriftRebalancer(Strategy): def initialize(self, parameters: Any = None) -> None: self.set_market(self.parameters.get("market", "NYSE")) self.sleeptime = self.parameters.get("sleeptime", "1D") - self.absolute_drift_threshold = Decimal(self.parameters.get("absolute_drift_threshold", "0.20")) + self.drift_threshold = Decimal(self.parameters.get("drift_threshold", "0.20")) self.acceptable_slippage = Decimal(self.parameters.get("acceptable_slippage", "0.005")) self.fill_sleeptime = self.parameters.get("fill_sleeptime", 15) self.target_weights = {k: Decimal(v) for k, v in self.parameters["target_weights"].items()} self.drift_df = pd.DataFrame() # Sanity checks - if self.acceptable_slippage >= self.absolute_drift_threshold: - raise ValueError("acceptable_slippage must be less than absolute_drift_threshold") - if self.absolute_drift_threshold >= Decimal("1.0"): - raise ValueError("absolute_drift_threshold must be less than 1.0") + if self.acceptable_slippage >= self.drift_threshold: + raise ValueError("acceptable_slippage must be less than drift_threshold") + if self.drift_threshold >= Decimal("1.0"): + raise ValueError("drift_threshold must be less than 1.0") for key, target_weight in self.target_weights.items(): - if self.absolute_drift_threshold >= target_weight: + if self.drift_threshold >= target_weight: logger.warning( - f"absolute_drift_threshold of {self.absolute_drift_threshold} is " + f"drift_threshold of {self.drift_threshold} is " f">= target_weight of {key}: {target_weight}. Drift in this asset will never trigger a rebalance." ) # noinspection PyAttributeOutsideInit def on_trading_iteration(self) -> None: dt = self.get_datetime() - logger.info(f"{dt} on_trading_iteration called") + msg = f"{dt} on_trading_iteration called" + logger.info(msg) + self.log_message(msg, broadcast=True) self.cancel_open_orders() if self.cash < 0: @@ -123,14 +125,17 @@ def on_trading_iteration(self) -> None: # Check if the absolute value of any drift is greater than the threshold rebalance_needed = False for index, row in self.drift_df.iterrows(): - if abs(row["absolute_drift"]) > self.absolute_drift_threshold: + msg = ( + f"Symbol: {row['symbol']} current_weight: {row['current_weight']:.2%} " + f"target_weight: {row['target_weight']:.2%} drift: {row['drift']:.2%}" + ) + if abs(row["drift"]) > self.drift_threshold: rebalance_needed = True - msg = ( - f"Absolute drift for {row['symbol']} is {abs(row['absolute_drift']):.2f} " - f"and exceeds threshold of {self.absolute_drift_threshold:.2f}" + msg += ( + f" Absolute drift exceeds threshold of {self.drift_threshold:.2%}. Rebalance needed." ) - logger.info(msg) - self.log_message(msg, broadcast=True) + logger.info(msg) + self.log_message(msg, broadcast=True) if rebalance_needed: msg = f"Rebalancing portfolio." @@ -167,7 +172,7 @@ def __init__(self, target_weights: Dict[str, Decimal]) -> None: "current_weight": Decimal(0), "target_weight": [Decimal(weight) for weight in target_weights.values()], "target_value": Decimal(0), - "absolute_drift": Decimal(0) + "drift": Decimal(0) }) def add_position(self, *, symbol: str, is_quote_asset: bool, current_quantity: Decimal, current_value: Decimal) -> None: @@ -184,7 +189,7 @@ def add_position(self, *, symbol: str, is_quote_asset: bool, current_quantity: D "current_weight": Decimal(0), "target_weight": Decimal(0), "target_value": Decimal(0), - "absolute_drift": Decimal(0) + "drift": Decimal(0) } # Convert the dictionary to a DataFrame new_row_df = pd.DataFrame([new_row]) @@ -212,7 +217,7 @@ def calculate_drift_row(row: pd.Series) -> Decimal: else: return row["target_weight"] - row["current_weight"] - self.df["absolute_drift"] = self.df.apply(calculate_drift_row, axis=1) + self.df["drift"] = self.df.apply(calculate_drift_row, axis=1) return self.df.copy() @@ -234,20 +239,21 @@ def rebalance(self) -> None: # Execute sells first sell_orders = [] for index, row in self.df.iterrows(): - if row["absolute_drift"] == -1: + if row["drift"] == -1: # Sell everything symbol = row["symbol"] quantity = row["current_quantity"] last_price = Decimal(self.strategy.get_last_price(symbol)) limit_price = self.calculate_limit_price(last_price=last_price, side="sell") - order = self.place_limit_order( - symbol=symbol, - quantity=quantity, - limit_price=limit_price, - side="sell" - ) - sell_orders.append(order) - elif row["absolute_drift"] < 0: + if quantity > 0: + order = self.place_limit_order( + symbol=symbol, + quantity=quantity, + limit_price=limit_price, + side="sell" + ) + sell_orders.append(order) + elif row["drift"] < 0: symbol = row["symbol"] last_price = Decimal(self.strategy.get_last_price(symbol)) limit_price = self.calculate_limit_price(last_price=last_price, side="sell") @@ -276,7 +282,7 @@ def rebalance(self) -> None: # Execute buys for index, row in self.df.iterrows(): - if row["absolute_drift"] > 0: + if row["drift"] > 0: symbol = row["symbol"] last_price = Decimal(self.strategy.get_last_price(symbol)) limit_price = self.calculate_limit_price(last_price=last_price, side="buy") diff --git a/tests/test_drift_rebalancer.py b/tests/test_drift_rebalancer.py index e295cf2cd..7a13d4352 100644 --- a/tests/test_drift_rebalancer.py +++ b/tests/test_drift_rebalancer.py @@ -96,7 +96,7 @@ def test_calculate_drift(self): assert df["target_value"].tolist() == [Decimal('1650.0'), Decimal('990.0'), Decimal('660.0')] - assert df["absolute_drift"].tolist() == [ + assert df["drift"].tolist() == [ Decimal('0.0454545454545454545454545455'), Decimal('-0.0030303030303030303030303030'), Decimal('-0.0424242424242424242424242424') @@ -144,7 +144,7 @@ def test_drift_is_negative_one_when_were_have_a_position_and_the_target_weights_ assert df["target_value"].tolist() == [Decimal("1650"), Decimal("990"), Decimal("0")] pd.testing.assert_series_equal( - df["absolute_drift"], + df["drift"], pd.Series([ Decimal('0.0454545454545454545454545455'), Decimal('-0.0030303030303030303030303030'), @@ -197,7 +197,7 @@ def test_drift_is_one_when_we_have_none_of_an_asset_and_target_weights_says_we_s assert df["target_value"].tolist() == [Decimal("825"), Decimal("825"), Decimal("825"), Decimal("825")] pd.testing.assert_series_equal( - df["absolute_drift"], + df["drift"], pd.Series([ Decimal('-0.2045454545454545454545454545'), Decimal('-0.0530303030303030303030303030'), @@ -256,7 +256,7 @@ def test_calculate_drift_when_quote_asset_position_exists(self): assert df["target_value"].tolist() == [Decimal("2150"), Decimal("1290"), Decimal("860"), Decimal("0")] pd.testing.assert_series_equal( - df["absolute_drift"], + df["drift"], pd.Series([ Decimal('0.1511627906976744186046511628'), Decimal('0.0674418604651162790697674419'), @@ -297,7 +297,7 @@ def test_calculate_drift_when_quote_asset_in_target_weights(self): assert df["current_weight"].tolist() == [Decimal("0.5"), Decimal("0.5"), Decimal("0.0")] assert df["target_value"].tolist() == [Decimal("250"), Decimal("250"), Decimal("500")] - assert df["absolute_drift"].tolist() == [Decimal("-0.25"), Decimal("-0.25"), Decimal("0")] + assert df["drift"].tolist() == [Decimal("-0.25"), Decimal("-0.25"), Decimal("0")] class MockStrategy(Strategy): @@ -337,7 +337,7 @@ def test_selling_everything(self): "current_quantity": [Decimal("10")], "current_value": [Decimal("1000")], "target_value": [Decimal("0")], - "absolute_drift": [Decimal("-1")] + "drift": [Decimal("-1")] }) executor = LimitOrderRebalanceLogic(strategy=strategy, df=df) executor.rebalance() @@ -352,7 +352,7 @@ def test_selling_part_of_a_holding(self): "current_quantity": [Decimal("10")], "current_value": [Decimal("1000")], "target_value": [Decimal("500")], - "absolute_drift": [Decimal("-0.5")] + "drift": [Decimal("-0.5")] }) executor = LimitOrderRebalanceLogic(strategy=strategy, df=df) executor.rebalance() @@ -360,6 +360,19 @@ def test_selling_part_of_a_holding(self): assert strategy.orders[0].side == "sell" assert strategy.orders[0].quantity == Decimal("5") + def test_selling_short_doesnt_create_and_order_when_shorting_is_disabled(self): + strategy = MockStrategy(broker=self.backtesting_broker) + df = pd.DataFrame({ + "symbol": ["AAPL"], + "current_quantity": [Decimal("0")], + "current_value": [Decimal("0")], + "target_value": [Decimal("-1000")], + "drift": [Decimal("-1")] + }) + executor = LimitOrderRebalanceLogic(strategy=strategy, df=df) + executor.rebalance() + assert len(strategy.orders) == 0 + def test_buying_something_when_we_have_enough_money_and_there_is_slippage(self): strategy = MockStrategy(broker=self.backtesting_broker) df = pd.DataFrame({ @@ -367,7 +380,7 @@ def test_buying_something_when_we_have_enough_money_and_there_is_slippage(self): "current_quantity": [Decimal("0")], "current_value": [Decimal("0")], "target_value": [Decimal("1000")], - "absolute_drift": [Decimal("1")] + "drift": [Decimal("1")] }) executor = LimitOrderRebalanceLogic(strategy=strategy, df=df) executor.rebalance() @@ -384,7 +397,7 @@ def test_buying_something_when_we_dont_have_enough_money_for_everything(self): "current_quantity": [Decimal("0")], "current_value": [Decimal("0")], "target_value": [Decimal("1000")], - "absolute_drift": [Decimal("1")] + "drift": [Decimal("1")] }) executor = LimitOrderRebalanceLogic(strategy=strategy, df=df) executor.rebalance() @@ -400,7 +413,7 @@ def test_attempting_to_buy_when_we_dont_have_enough_money_for_even_one_share(sel "current_quantity": [Decimal("0")], "current_value": [Decimal("0")], "target_value": [Decimal("1000")], - "absolute_drift": [Decimal("1")] + "drift": [Decimal("1")] }) executor = LimitOrderRebalanceLogic(strategy=strategy, df=df) executor.rebalance() @@ -413,7 +426,7 @@ def test_attempting_to_sell_when_the_amount_we_need_to_sell_is_less_than_the_lim "current_quantity": [Decimal("1")], "current_value": [Decimal("100")], "target_value": [Decimal("10")], - "absolute_drift": [Decimal("-0.5")] + "drift": [Decimal("-0.5")] }) executor = LimitOrderRebalanceLogic(strategy=strategy, df=df) executor.rebalance() @@ -426,7 +439,7 @@ def test_calculate_limit_price_when_selling(self): "current_quantity": [Decimal("10")], "current_value": [Decimal("1000")], "target_value": [Decimal("0")], - "absolute_drift": [Decimal("-1")] + "drift": [Decimal("-1")] }) executor = LimitOrderRebalanceLogic(strategy=strategy, df=df, acceptable_slippage=Decimal("0.005")) limit_price = executor.calculate_limit_price(last_price=Decimal("120.00"), side="sell") @@ -439,7 +452,7 @@ def test_calculate_limit_price_when_buying(self): "current_quantity": [Decimal("10")], "current_value": [Decimal("1000")], "target_value": [Decimal("0")], - "absolute_drift": [Decimal("-1")] + "drift": [Decimal("-1")] }) executor = LimitOrderRebalanceLogic(strategy=strategy, df=df, acceptable_slippage=Decimal("0.005")) limit_price = executor.calculate_limit_price(last_price=Decimal("120.00"), side="buy") @@ -458,7 +471,7 @@ def test_classic_60_60(self, pandas_data_fixture): parameters = { "market": "NYSE", "sleeptime": "1D", - "absolute_drift_threshold": "0.03", + "drift_threshold": "0.03", "acceptable_slippage": "0.005", "fill_sleeptime": 15, "target_weights": { @@ -479,7 +492,7 @@ def test_classic_60_60(self, pandas_data_fixture): show_indicators=False, save_logfile=False, show_progress_bar=False, - quiet_logs=True, + quiet_logs=False, ) assert results is not None From b263cf733a47187bfabd31bc4e55b966914711ae Mon Sep 17 00:00:00 2001 From: Brett Elliot Date: Tue, 5 Nov 2024 06:37:52 -0500 Subject: [PATCH 5/8] enabling or preventing shorting in the drift rebalancer --- lumibot/strategies/drift_rebalancer.py | 18 +++-- tests/test_drift_rebalancer.py | 92 +++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/lumibot/strategies/drift_rebalancer.py b/lumibot/strategies/drift_rebalancer.py index 3dcc40905..dfe0da06e 100644 --- a/lumibot/strategies/drift_rebalancer.py +++ b/lumibot/strategies/drift_rebalancer.py @@ -145,7 +145,8 @@ def on_trading_iteration(self) -> None: strategy=self, df=self.drift_df, fill_sleeptime=self.fill_sleeptime, - acceptable_slippage=self.acceptable_slippage + acceptable_slippage=self.acceptable_slippage, + shorting=False ) rebalance_logic.rebalance() @@ -210,10 +211,16 @@ def calculate_drift_row(row: pd.Series) -> Decimal: if row["is_quote_asset"]: # We can never buy or sell the quote asset return Decimal(0) + + # Check if we should sell everything elif row["current_quantity"] > Decimal(0) and row["target_weight"] == Decimal(0): return Decimal(-1) + + # Check if we need to buy for the first time elif row["current_quantity"] == Decimal(0) and row["target_weight"] > Decimal(0): return Decimal(1) + + # Otherwise we just need to adjust our holding else: return row["target_weight"] - row["current_weight"] @@ -228,12 +235,14 @@ def __init__( strategy: Strategy, df: pd.DataFrame, fill_sleeptime: int = 15, - acceptable_slippage: Decimal = Decimal("0.005") + acceptable_slippage: Decimal = Decimal("0.005"), + shorting: bool = False ) -> None: self.strategy = strategy self.df = df self.fill_sleeptime = fill_sleeptime self.acceptable_slippage = acceptable_slippage + self.shorting = shorting def rebalance(self) -> None: # Execute sells first @@ -245,7 +254,7 @@ def rebalance(self) -> None: quantity = row["current_quantity"] last_price = Decimal(self.strategy.get_last_price(symbol)) limit_price = self.calculate_limit_price(last_price=last_price, side="sell") - if quantity > 0: + if quantity > 0 or (quantity == 0 and self.shorting): order = self.place_limit_order( symbol=symbol, quantity=quantity, @@ -253,12 +262,13 @@ def rebalance(self) -> None: side="sell" ) sell_orders.append(order) + elif row["drift"] < 0: symbol = row["symbol"] last_price = Decimal(self.strategy.get_last_price(symbol)) limit_price = self.calculate_limit_price(last_price=last_price, side="sell") quantity = ((row["current_value"] - row["target_value"]) / limit_price).quantize(Decimal('1'), rounding=ROUND_DOWN) - if quantity > 0: + if quantity > 0 and (quantity < row["current_quantity"] or self.shorting): order = self.place_limit_order( symbol=symbol, quantity=quantity, diff --git a/tests/test_drift_rebalancer.py b/tests/test_drift_rebalancer.py index 7a13d4352..1e36faa8e 100644 --- a/tests/test_drift_rebalancer.py +++ b/tests/test_drift_rebalancer.py @@ -102,7 +102,7 @@ def test_calculate_drift(self): Decimal('-0.0424242424242424242424242424') ] - def test_drift_is_negative_one_when_were_have_a_position_and_the_target_weights_says_to_not_have_it(self): + def test_drift_is_negative_one_when_we_have_a_position_and_the_target_weights_says_to_not_have_it(self): target_weights = { "AAPL": Decimal("0.5"), "GOOGL": Decimal("0.3"), @@ -299,6 +299,57 @@ def test_calculate_drift_when_quote_asset_in_target_weights(self): assert df["target_value"].tolist() == [Decimal("250"), Decimal("250"), Decimal("500")] assert df["drift"].tolist() == [Decimal("-0.25"), Decimal("-0.25"), Decimal("0")] + def test_calculate_drift_when_we_want_short_something(self): + target_weights = { + "AAPL": Decimal("-0.50"), + "USD": Decimal("0.50") + } + self.calculator = DriftCalculationLogic(target_weights=target_weights) + self.calculator.add_position( + symbol="USD", + is_quote_asset=True, + current_quantity=Decimal("1000"), + current_value=Decimal("1000") + ) + self.calculator.add_position( + symbol="AAPL", + is_quote_asset=False, + current_quantity=Decimal("0"), + current_value=Decimal("0") + ) + + df = self.calculator.calculate() + print(f"\n{df}") + + assert df["current_weight"].tolist() == [Decimal("0.0"), Decimal("1.0")] + assert df["target_value"].tolist() == [Decimal("-500"), Decimal("500")] + assert df["drift"].tolist() == [Decimal("-0.50"), Decimal("0")] + + def test_calculate_drift_when_we_want_a_100_percent_short_position(self): + target_weights = { + "AAPL": Decimal("-1.0"), + "USD": Decimal("0.0") + } + self.calculator = DriftCalculationLogic(target_weights=target_weights) + self.calculator.add_position( + symbol="USD", + is_quote_asset=True, + current_quantity=Decimal("1000"), + current_value=Decimal("1000") + ) + self.calculator.add_position( + symbol="AAPL", + is_quote_asset=False, + current_quantity=Decimal("0"), + current_value=Decimal("0") + ) + + df = self.calculator.calculate() + + assert df["current_weight"].tolist() == [Decimal("0.0"), Decimal("1.0")] + assert df["target_value"].tolist() == [Decimal("-1000"), Decimal("0")] + assert df["drift"].tolist() == [Decimal("-1.0"), Decimal("0")] + class MockStrategy(Strategy): @@ -373,6 +424,45 @@ def test_selling_short_doesnt_create_and_order_when_shorting_is_disabled(self): executor.rebalance() assert len(strategy.orders) == 0 + def test_selling_small_short_position_creates_and_order_when_shorting_is_enabled(self): + strategy = MockStrategy(broker=self.backtesting_broker) + df = pd.DataFrame({ + "symbol": ["AAPL"], + "current_quantity": [Decimal("0")], + "current_value": [Decimal("0")], + "target_value": [Decimal("-1000")], + "drift": [Decimal("-0.25")] + }) + executor = LimitOrderRebalanceLogic(strategy=strategy, df=df, shorting=True) + executor.rebalance() + assert len(strategy.orders) == 1 + + def test_selling_small_short_position_doesnt_creatne_order_when_shorting_is_disabled(self): + strategy = MockStrategy(broker=self.backtesting_broker) + df = pd.DataFrame({ + "symbol": ["AAPL"], + "current_quantity": [Decimal("0")], + "current_value": [Decimal("0")], + "target_value": [Decimal("-1000")], + "drift": [Decimal("-0.25")] + }) + executor = LimitOrderRebalanceLogic(strategy=strategy, df=df, shorting=False) + executor.rebalance() + assert len(strategy.orders) == 0 + + def test_selling_a_100_percent_short_position_creates_and_order_when_shorting_is_enabled(self): + strategy = MockStrategy(broker=self.backtesting_broker) + df = pd.DataFrame({ + "symbol": ["AAPL"], + "current_quantity": [Decimal("0")], + "current_value": [Decimal("0")], + "target_value": [Decimal("-1000")], + "drift": [Decimal("-1")] + }) + executor = LimitOrderRebalanceLogic(strategy=strategy, df=df, shorting=True) + executor.rebalance() + assert len(strategy.orders) == 1 + def test_buying_something_when_we_have_enough_money_and_there_is_slippage(self): strategy = MockStrategy(broker=self.backtesting_broker) df = pd.DataFrame({ From 367f8f34f9a2de45ce3835c0ced3fc1e6f47b4f7 Mon Sep 17 00:00:00 2001 From: Brett Elliot Date: Tue, 5 Nov 2024 06:42:46 -0500 Subject: [PATCH 6/8] add shorting strategy param --- lumibot/example_strategies/classic_60_40.py | 3 ++- lumibot/strategies/drift_rebalancer.py | 6 +++++- tests/test_drift_rebalancer.py | 7 ++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lumibot/example_strategies/classic_60_40.py b/lumibot/example_strategies/classic_60_40.py index 14ad3a199..99c6186d0 100644 --- a/lumibot/example_strategies/classic_60_40.py +++ b/lumibot/example_strategies/classic_60_40.py @@ -23,7 +23,8 @@ "target_weights": { "SPY": "0.60", "TLT": "0.40" - } + }, + "shorting": False } if is_live: diff --git a/lumibot/strategies/drift_rebalancer.py b/lumibot/strategies/drift_rebalancer.py index dfe0da06e..ec40f7316 100644 --- a/lumibot/strategies/drift_rebalancer.py +++ b/lumibot/strategies/drift_rebalancer.py @@ -64,6 +64,9 @@ class DriftRebalancer(Strategy): "TLT": "0.40", "USD": "0.00", } + + # If you want to allow shorting, set this to True. + shorting: False } """ @@ -75,6 +78,7 @@ def initialize(self, parameters: Any = None) -> None: self.acceptable_slippage = Decimal(self.parameters.get("acceptable_slippage", "0.005")) self.fill_sleeptime = self.parameters.get("fill_sleeptime", 15) self.target_weights = {k: Decimal(v) for k, v in self.parameters["target_weights"].items()} + self.shorting = self.parameters.get("shorting", False) self.drift_df = pd.DataFrame() # Sanity checks @@ -146,7 +150,7 @@ def on_trading_iteration(self) -> None: df=self.drift_df, fill_sleeptime=self.fill_sleeptime, acceptable_slippage=self.acceptable_slippage, - shorting=False + shorting=self.shorting ) rebalance_logic.rebalance() diff --git a/tests/test_drift_rebalancer.py b/tests/test_drift_rebalancer.py index 1e36faa8e..4d130bf43 100644 --- a/tests/test_drift_rebalancer.py +++ b/tests/test_drift_rebalancer.py @@ -567,7 +567,8 @@ def test_classic_60_60(self, pandas_data_fixture): "target_weights": { "SPY": "0.60", "TLT": "0.40" - } + }, + "shorting": False } results, strat_obj = DriftRebalancer.run_backtest( @@ -590,3 +591,7 @@ def test_classic_60_60(self, pandas_data_fixture): assert np.isclose(results["volatility"], 0.06740737779031068, atol=1e-4) assert np.isclose(results["sharpe"], 3.051823053251843, atol=1e-4) assert np.isclose(results["max_drawdown"]["drawdown"], 0.025697778711759052, atol=1e-4) + + def test_with_shorting(self): + # TODO + pass From 5dad9695e4a81a59c474bb612a9adc7012b95455 Mon Sep 17 00:00:00 2001 From: Brett Elliot Date: Thu, 7 Nov 2024 16:30:13 -0500 Subject: [PATCH 7/8] added tests for bars index being timestamp --- tests/test_bars.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/test_bars.py b/tests/test_bars.py index 811333ec1..239819919 100644 --- a/tests/test_bars.py +++ b/tests/test_bars.py @@ -1,5 +1,5 @@ import os -import datetime +from datetime import datetime, timedelta import logging import pytest @@ -29,8 +29,8 @@ class TestBarsContainReturns: """These tests check that the bars from get_historical_prices contain returns for the different data sources.""" expected_df = None - backtesting_start = datetime.datetime(2019, 3, 1) - backtesting_end = datetime.datetime(2019, 3, 31) + backtesting_start = datetime(2019, 3, 1) + backtesting_end = datetime(2019, 3, 31) @classmethod def setup_class(cls): @@ -53,6 +53,8 @@ def test_alpaca_data_source_generates_simple_returns(self): data_source = AlpacaData(ALPACA_CONFIG) prices = data_source.get_historical_prices("SPY", 2, "day") + assert isinstance(prices.df.index[0], pd.Timestamp) + # assert that the last row has a return value assert prices.df["return"].iloc[-1] is not None @@ -64,8 +66,8 @@ def test_yahoo_data_source_generates_adjusted_returns(self): This tests that the yahoo data_source calculates adjusted returns for bars and that they are calculated correctly. """ - start = self.backtesting_start + datetime.timedelta(days=25) - end = self.backtesting_end + datetime.timedelta(days=25) + start = self.backtesting_start + timedelta(days=25) + end = self.backtesting_end + timedelta(days=25) data_source = YahooData(datetime_start=start, datetime_end=end) prices = data_source.get_historical_prices("SPY", 25, "day") @@ -109,8 +111,8 @@ def test_pandas_data_source_generates_adjusted_returns(self, pandas_data_fixture This tests that the pandas data_source calculates adjusted returns for bars and that they are calculated correctly. It assumes that it is provided split adjusted OHLCV and dividend data. """ - start = self.backtesting_start + datetime.timedelta(days=25) - end = self.backtesting_end + datetime.timedelta(days=25) + start = self.backtesting_start + timedelta(days=25) + end = self.backtesting_end + timedelta(days=25) data_source = PandasData( datetime_start=start, datetime_end=end, @@ -118,7 +120,8 @@ def test_pandas_data_source_generates_adjusted_returns(self, pandas_data_fixture ) prices = data_source.get_historical_prices("SPY", 25, "day") - # assert that the last row has a return value + assert isinstance(prices.df.index[0], pd.Timestamp) + assert prices.df["return"].iloc[-1] is not None # check that there is a dividend column. @@ -160,8 +163,8 @@ def test_polygon_data_source_generates_simple_returns(self): alpaca, we are not going to check if the returns are adjusted correctly. """ # get data from 3 months ago, so we can use the free Polygon.io data - start = datetime.datetime.now() - datetime.timedelta(days=90) - end = datetime.datetime.now() - datetime.timedelta(days=60) + start = datetime.now() - timedelta(days=90) + end = datetime.now() - timedelta(days=60) tzinfo = pytz.timezone("America/New_York") start = start.astimezone(tzinfo) end = end.astimezone(tzinfo) @@ -174,6 +177,8 @@ def test_polygon_data_source_generates_simple_returns(self): # assert that the last row has a return value assert prices.df["return"].iloc[-1] is not None + assert isinstance(prices.df.index[0], pd.Timestamp) + @pytest.mark.skipif(not TRADIER_CONFIG['ACCESS_TOKEN'], reason="No Tradier credentials provided.") def test_tradier_data_source_generates_simple_returns(self): """ @@ -188,5 +193,8 @@ def test_tradier_data_source_generates_simple_returns(self): spy_asset = Asset("SPY") prices = data_source.get_historical_prices(spy_asset, 2, "day") + # This shows a bug. The index a datetime.date but should be a timestamp + # assert isinstance(prices.df.index[0], pd.Timestamp) + # assert that the last row has a return value assert prices.df["return"].iloc[-1] is not None From a4f70cff3f4ae0a14ea1b107b52bd70df7833e6d Mon Sep 17 00:00:00 2001 From: Brett Elliot Date: Thu, 7 Nov 2024 17:02:17 -0500 Subject: [PATCH 8/8] move driftRebalancer to examples; change example to be more lumibot like; remove logger in strategy --- README.md | 1 + lumibot/example_strategies/classic_60_40.py | 49 +++++++++---------- .../drift_rebalancer.py | 39 +++++++++------ tests/test_drift_rebalancer.py | 12 ++--- 4 files changed, 50 insertions(+), 51 deletions(-) rename lumibot/{strategies => example_strategies}/drift_rebalancer.py (91%) diff --git a/README.md b/README.md index 68ffed417..69bfc2848 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,7 @@ git fetch origin git merge origin/dev git checkout my-feature git rebase dev +git checkout my-feature git push --force-with-lease origin my-feature ``` diff --git a/lumibot/example_strategies/classic_60_40.py b/lumibot/example_strategies/classic_60_40.py index 99c6186d0..9d4976999 100644 --- a/lumibot/example_strategies/classic_60_40.py +++ b/lumibot/example_strategies/classic_60_40.py @@ -1,45 +1,40 @@ from datetime import datetime -from lumibot.strategies.drift_rebalancer import DriftRebalancer +from lumibot.credentials import IS_BACKTESTING +from lumibot.example_strategies.drift_rebalancer import DriftRebalancer """ Strategy Description -This strategy rebalances a portfolio of assets to a target weight every time the asset drifts +This strategy demonstrates the DriftRebalancer by rebalancing to a classic 60% stocks, 40% bonds portfolio. +It rebalances a portfolio of assets to a target weight every time the asset drifts by a certain threshold. The strategy will sell the assets that has drifted the most and buy the assets that has drifted the least to bring the portfolio back to the target weights. """ if __name__ == "__main__": - is_live = False - - parameters = { - "market": "NYSE", - "sleeptime": "1D", - "drift_threshold": "0.15", - "acceptable_slippage": "0.005", # 50 BPS - "fill_sleeptime": 15, - "target_weights": { - "SPY": "0.60", - "TLT": "0.40" - }, - "shorting": False - } - - if is_live: - from credentials import ALPACA_CONFIG - from lumibot.brokers import Alpaca - from lumibot.traders import Trader - - trader = Trader() - broker = Alpaca(ALPACA_CONFIG) - strategy = DriftRebalancer(broker=broker, parameters=parameters) - trader.add_strategy(strategy) - strategy_executors = trader.run_all() + if not IS_BACKTESTING: + print("This strategy is not meant to be run live. Please set IS_BACKTESTING to True.") + exit() else: + + parameters = { + "market": "NYSE", + "sleeptime": "1D", + "drift_threshold": "0.05", + "acceptable_slippage": "0.005", # 50 BPS + "fill_sleeptime": 15, + "target_weights": { + "SPY": "0.60", + "TLT": "0.40" + }, + "shorting": False + } + from lumibot.backtesting import YahooDataBacktesting + backtesting_start = datetime(2023, 1, 2) backtesting_end = datetime(2024, 10, 31) diff --git a/lumibot/strategies/drift_rebalancer.py b/lumibot/example_strategies/drift_rebalancer.py similarity index 91% rename from lumibot/strategies/drift_rebalancer.py rename to lumibot/example_strategies/drift_rebalancer.py index ec40f7316..4e7a0c2ad 100644 --- a/lumibot/strategies/drift_rebalancer.py +++ b/lumibot/example_strategies/drift_rebalancer.py @@ -2,14 +2,9 @@ from typing import Dict, Any from decimal import Decimal, ROUND_DOWN import time -import logging from lumibot.strategies.strategy import Strategy -logger = logging.getLogger(__name__) -# print_full_pandas_dataframes() -# set_pandas_float_precision(precision=15) - """ The DriftRebalancer strategy is designed to maintain a portfolio's target asset allocation by rebalancing assets based on their drift from target weights. The strategy calculates the @@ -88,7 +83,7 @@ def initialize(self, parameters: Any = None) -> None: raise ValueError("drift_threshold must be less than 1.0") for key, target_weight in self.target_weights.items(): if self.drift_threshold >= target_weight: - logger.warning( + self.logger.warning( f"drift_threshold of {self.drift_threshold} is " f">= target_weight of {key}: {target_weight}. Drift in this asset will never trigger a rebalance." ) @@ -97,12 +92,12 @@ def initialize(self, parameters: Any = None) -> None: def on_trading_iteration(self) -> None: dt = self.get_datetime() msg = f"{dt} on_trading_iteration called" - logger.info(msg) + self.logger.info(msg) self.log_message(msg, broadcast=True) self.cancel_open_orders() if self.cash < 0: - logger.error(f"Negative cash: {self.cash} but DriftRebalancer does not support short sales or margin yet.") + self.logger.error(f"Negative cash: {self.cash} but DriftRebalancer does not support short sales or margin yet.") drift_calculator = DriftCalculationLogic(target_weights=self.target_weights) @@ -138,12 +133,12 @@ def on_trading_iteration(self) -> None: msg += ( f" Absolute drift exceeds threshold of {self.drift_threshold:.2%}. Rebalance needed." ) - logger.info(msg) + self.logger.info(msg) self.log_message(msg, broadcast=True) if rebalance_needed: msg = f"Rebalancing portfolio." - logger.info(msg) + self.logger.info(msg) self.log_message(msg, broadcast=True) rebalance_logic = LimitOrderRebalanceLogic( strategy=self, @@ -156,13 +151,13 @@ def on_trading_iteration(self) -> None: def on_abrupt_closing(self): dt = self.get_datetime() - logger.info(f"{dt} on_abrupt_closing called") + self.logger.info(f"{dt} on_abrupt_closing called") self.log_message("On abrupt closing called.", broadcast=True) self.cancel_open_orders() def on_bot_crash(self, error): dt = self.get_datetime() - logger.info(f"{dt} on_bot_crash called") + self.logger.info(f"{dt} on_bot_crash called") self.log_message(f"Bot crashed with error: {error}", broadcast=True) self.cancel_open_orders() @@ -251,6 +246,7 @@ def __init__( def rebalance(self) -> None: # Execute sells first sell_orders = [] + buy_orders = [] for index, row in self.df.iterrows(): if row["drift"] == -1: # Sell everything @@ -282,14 +278,14 @@ def rebalance(self) -> None: sell_orders.append(order) for order in sell_orders: - logger.info(f"Submitted sell order: {order}") + self.strategy.logger.info(f"Submitted sell order: {order}") if not self.strategy.is_backtesting: # Sleep to allow sell orders to fill time.sleep(self.fill_sleeptime) orders = self.strategy.broker._pull_all_orders(self.strategy.name, self.strategy) for order in orders: - logger.info(f"Order at broker: {order}") + self.strategy.logger.info(f"Order at broker: {order}") # Get current cash position from the broker cash_position = self.get_current_cash_position() @@ -303,10 +299,21 @@ def rebalance(self) -> None: order_value = row["target_value"] - row["current_value"] quantity = (min(order_value, cash_position) / limit_price).quantize(Decimal('1'), rounding=ROUND_DOWN) if quantity > 0: - self.place_limit_order(symbol=symbol, quantity=quantity, limit_price=limit_price, side="buy") + order = self.place_limit_order(symbol=symbol, quantity=quantity, limit_price=limit_price, side="buy") + buy_orders.append(order) cash_position -= min(order_value, cash_position) else: - logger.info(f"Ran out of cash to buy {symbol}. Cash: {cash_position} and limit_price: {limit_price:.2f}") + self.strategy.logger.info(f"Ran out of cash to buy {symbol}. Cash: {cash_position} and limit_price: {limit_price:.2f}") + + for order in buy_orders: + self.strategy.logger.info(f"Submitted buy order: {order}") + + if not self.strategy.is_backtesting: + # Sleep to allow orders to fill + time.sleep(self.fill_sleeptime) + orders = self.strategy.broker._pull_all_orders(self.strategy.name, self.strategy) + for order in orders: + self.strategy.logger.info(f"Order at broker: {order}") def calculate_limit_price(self, *, last_price: Decimal, side: str) -> Decimal: if side == "sell": diff --git a/tests/test_drift_rebalancer.py b/tests/test_drift_rebalancer.py index 4d130bf43..3d8cc3e3d 100644 --- a/tests/test_drift_rebalancer.py +++ b/tests/test_drift_rebalancer.py @@ -2,20 +2,16 @@ from typing import Any import datetime import logging -import pytest import pandas as pd import numpy as np -from lumibot.strategies.drift_rebalancer import DriftCalculationLogic -from lumibot.strategies.drift_rebalancer import LimitOrderRebalanceLogic, Strategy -from lumibot.entities import Asset, Order +from lumibot.example_strategies.drift_rebalancer import DriftCalculationLogic, LimitOrderRebalanceLogic, DriftRebalancer from lumibot.backtesting import BacktestingBroker, YahooDataBacktesting, PandasDataBacktesting -from lumibot.strategies.drift_rebalancer import DriftRebalancer +from lumibot.strategies.strategy import Strategy from tests.fixtures import pandas_data_fixture from lumibot.tools import print_full_pandas_dataframes, set_pandas_float_precision -logger = logging.getLogger(__name__) print_full_pandas_dataframes() set_pandas_float_precision(precision=5) @@ -319,7 +315,7 @@ def test_calculate_drift_when_we_want_short_something(self): ) df = self.calculator.calculate() - print(f"\n{df}") + # print(f"\n{df}") assert df["current_weight"].tolist() == [Decimal("0.0"), Decimal("1.0")] assert df["target_value"].tolist() == [Decimal("-500"), Decimal("500")] @@ -583,7 +579,7 @@ def test_classic_60_60(self, pandas_data_fixture): show_indicators=False, save_logfile=False, show_progress_bar=False, - quiet_logs=False, + # quiet_logs=False, ) assert results is not None