diff --git a/README.md b/README.md index 69bfc2848..5814bb7b3 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,11 @@ To run this example strategy, click on the `Deploy to Render` button below to de If you want to contribute to Lumibot, you can check how to get started below. We are always looking for contributors to help us out! +Here's a video to help you get started with contributing to Lumibot: [Watch The Video](https://youtu.be/Huz6VxqafZs) + **Steps to contribute:** +0. Watch the video: [Watch The Video](https://youtu.be/Huz6VxqafZs) 1. Clone the repository to your local machine 2. Create a new branch for your feature 3. Run `pip install -r requirements_dev.txt` to install the developer dependencies diff --git a/lumibot/brokers/interactive_brokers_rest.py b/lumibot/brokers/interactive_brokers_rest.py index d1d934be4..8fcb096dc 100644 --- a/lumibot/brokers/interactive_brokers_rest.py +++ b/lumibot/brokers/interactive_brokers_rest.py @@ -1190,4 +1190,4 @@ def _get_broker_id_from_raw_orders(self, raw_orders): for leg in o["leg"]: if "orderId" in leg: ids.append(str(leg["orderId"])) - return ids + return ids \ No newline at end of file diff --git a/lumibot/components/drift_rebalancer_logic.py b/lumibot/components/drift_rebalancer_logic.py index eae2f0bfc..fdeec0b68 100644 --- a/lumibot/components/drift_rebalancer_logic.py +++ b/lumibot/components/drift_rebalancer_logic.py @@ -1,4 +1,3 @@ -from abc import ABC, abstractmethod from typing import Dict, Any from decimal import Decimal, ROUND_DOWN import time @@ -7,6 +6,7 @@ from lumibot.strategies.strategy import Strategy from lumibot.entities.order import Order +from lumibot.tools.pandas import prettify_dataframe_with_decimals class DriftType: @@ -123,7 +123,7 @@ def __init__( *, strategy: Strategy, drift_type: DriftType = DriftType.ABSOLUTE, - drift_threshold: Decimal = Decimal("0.05") + drift_threshold: Decimal = Decimal("0.05"), ) -> None: self.strategy = strategy self.drift_type = drift_type @@ -133,13 +133,14 @@ def __init__( def calculate(self, target_weights: Dict[str, Decimal]) -> pd.DataFrame: if self.drift_type == DriftType.ABSOLUTE: - # Make sure the target_weights are all less than the drift threshold - for key, target_weight in target_weights.items(): - if self.drift_threshold >= target_weight: - self.strategy.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." - ) + # The absolute value of all the weights are less than the drift_threshold + # then we will never trigger a rebalance. + + if all([abs(weight) < self.drift_threshold for weight in target_weights.values()]): + self.strategy.logger.warning( + f"All target weights are less than the drift_threshold: {self.drift_threshold}. " + f"No rebalance will be triggered." + ) self.df = pd.DataFrame({ "symbol": target_weights.keys(), @@ -229,7 +230,7 @@ def _calculate_drift_row(self, row: pd.Series) -> Decimal: return Decimal(-1) elif row["current_quantity"] == Decimal(0) and row["target_weight"] > Decimal(0): - # We don't have any of this asset but we wanna buy some. + # We don't have any of this asset, but we want to buy some. return Decimal(1) elif row["current_quantity"] == Decimal(0) and row["target_weight"] == Decimal(-1): @@ -237,7 +238,7 @@ def _calculate_drift_row(self, row: pd.Series) -> Decimal: return Decimal(-1) elif row["current_quantity"] == Decimal(0) and row["target_weight"] < Decimal(0): - # We don't have any of this asset but we wanna short some. + # We don't have any of this asset, but we want to short some. return Decimal(-1) # Otherwise we just need to adjust our holding. Calculate the drift. @@ -284,6 +285,10 @@ def rebalance(self, drift_df: pd.DataFrame = None) -> bool: if drift_df is None: raise ValueError("You must pass in a DataFrame to DriftOrderLogic.rebalance()") + # Just print the drift_df to the log but sort it by symbol column + drift_df = drift_df.sort_values(by='symbol') + self.strategy.logger.info(f"drift_df:\n{prettify_dataframe_with_decimals(df=drift_df)}") + rebalance_needed = self._check_if_rebalance_needed(drift_df) if rebalance_needed: self._rebalance(drift_df) @@ -293,6 +298,9 @@ def _rebalance(self, df: pd.DataFrame = None) -> None: if df is None: raise ValueError("You must pass in a DataFrame to DriftOrderLogic.rebalance()") + # sort dataframe by the largest absolute value drift first + df = df.reindex(df["drift"].abs().sort_values(ascending=False).index) + # Execute sells first sell_orders = [] buy_orders = [] @@ -332,20 +340,12 @@ def _rebalance(self, df: pd.DataFrame = None) -> None: ) sell_orders.append(order) - for order in sell_orders: - 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) - try: - for order in sell_orders: - pulled_order = self.strategy.broker._pull_order(order.identifier, self.strategy.name) - msg = f"Status of submitted sell order: {pulled_order}" - self.strategy.logger.info(msg) - self.strategy.log_message(msg, broadcast=True) - except Exception as e: - self.strategy.logger.error(f"Error pulling order: {e}") + + for order in sell_orders: + self.strategy.logger.info(f"Submitted sell order: {order}") # Get current cash position from the broker cash_position = self.get_current_cash_position() @@ -365,23 +365,13 @@ def _rebalance(self, df: pd.DataFrame = None) -> None: cash_position -= min(order_value, cash_position) else: self.strategy.logger.info( - f"Ran out of cash to buy {symbol}. Cash: {cash_position} and limit_price: {limit_price:.2f}") + f"Ran out of cash to buy {symbol}. " + f"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 sell orders to fill - time.sleep(self.fill_sleeptime) - try: - for order in buy_orders: - pulled_order = self.strategy.broker._pull_order(order.identifier, self.strategy.name) - msg = f"Status of submitted buy order: {pulled_order}" - self.strategy.logger.info(msg) - self.strategy.log_message(msg, broadcast=True) - except Exception as e: - self.strategy.logger.error(f"Error pulling order: {e}") - def calculate_limit_price(self, *, last_price: Decimal, side: str) -> Decimal: if side == "sell": return last_price * (1 - self.acceptable_slippage) @@ -406,7 +396,9 @@ def place_order(self, *, symbol: str, quantity: Decimal, limit_price: Decimal, s quantity=quantity, side=side ) - return self.strategy.submit_order(order) + + self.strategy.submit_order(order) + return order def _check_if_rebalance_needed(self, drift_df: pd.DataFrame) -> bool: # Check if the absolute value of any drift is greater than the threshold @@ -419,8 +411,8 @@ def _check_if_rebalance_needed(self, drift_df: pd.DataFrame) -> bool: if abs(row["drift"]) > self.drift_threshold: rebalance_needed = True msg += ( - f" Absolute drift exceeds threshold of {self.drift_threshold:.2%}. Rebalance needed." + f" Drift exceeds threshold." ) self.strategy.logger.info(msg) - self.strategy.log_message(msg, broadcast=True) + return rebalance_needed diff --git a/lumibot/data_sources/interactive_brokers_rest_data.py b/lumibot/data_sources/interactive_brokers_rest_data.py index e298bd569..9edfcc4ad 100644 --- a/lumibot/data_sources/interactive_brokers_rest_data.py +++ b/lumibot/data_sources/interactive_brokers_rest_data.py @@ -1,6 +1,6 @@ import logging from termcolor import colored -from lumibot.entities import Asset, Bars +from ..entities import Asset, Bars from .data_source import DataSource import subprocess @@ -1078,4 +1078,4 @@ def get_quote(self, asset, quote=None, exchange=None): else: result["ask"] = None - return result + return result \ No newline at end of file diff --git a/lumibot/data_sources/pandas_data.py b/lumibot/data_sources/pandas_data.py index 548166e75..1c83e5568 100644 --- a/lumibot/data_sources/pandas_data.py +++ b/lumibot/data_sources/pandas_data.py @@ -427,7 +427,14 @@ def get_start_datetime_and_ts_unit(self, length, timestep, start_dt=None, start_ return start_datetime, ts_unit def get_historical_prices( - self, asset, length, timestep="", timeshift=None, quote=None, exchange=None, include_after_hours=True + self, + asset: Asset, + length: int, + timestep: str = None, + timeshift: int = None, + quote: Asset = None, + exchange: str = None, + include_after_hours: bool = True, ): """Get bars for a given asset""" if isinstance(asset, str): diff --git a/lumibot/data_sources/tradier_data.py b/lumibot/data_sources/tradier_data.py index aab051de5..67ed0b851 100644 --- a/lumibot/data_sources/tradier_data.py +++ b/lumibot/data_sources/tradier_data.py @@ -267,20 +267,26 @@ def get_last_price(self, asset, quote=None, exchange=None): Price of the asset """ - if asset.asset_type == "option": - symbol = create_options_symbol( - asset.symbol, - asset.expiration, - asset.right, - asset.strike, - ) - elif asset.asset_type == "index": - symbol = f"I:{asset.symbol}" - else: - symbol = asset.symbol + symbol = None + try: + if asset.asset_type == "option": + symbol = create_options_symbol( + asset.symbol, + asset.expiration, + asset.right, + asset.strike, + ) + elif asset.asset_type == "index": + symbol = f"I:{asset.symbol}" + else: + symbol = asset.symbol - price = self.tradier.market.get_last_price(symbol) - return price + price = self.tradier.market.get_last_price(symbol) + return price + + except Exception as e: + logging.error(f"Error getting last price for {symbol or asset.symbol}: {e}") + return None def get_quote(self, asset, quote=None, exchange=None): """ diff --git a/lumibot/entities/data.py b/lumibot/entities/data.py index dbc15018e..b040e7e1b 100644 --- a/lumibot/entities/data.py +++ b/lumibot/entities/data.py @@ -596,7 +596,7 @@ def get_bars(self, dt, length=1, timestep=MIN_TIMESTEP, timeshift=0): # The original df_result may include more rows when timestep is day and self.timestep is minute. # In this case, we only want to return the last n rows. - df_result = df_result.tail(n=num_periods) + df_result = df_result.tail(n=int(num_periods)) return df_result diff --git a/lumibot/example_strategies/drift_rebalancer.py b/lumibot/example_strategies/drift_rebalancer.py index 8af139304..fadbcdde5 100644 --- a/lumibot/example_strategies/drift_rebalancer.py +++ b/lumibot/example_strategies/drift_rebalancer.py @@ -94,6 +94,7 @@ def initialize(self, parameters: Any = None) -> None: 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.verbose = self.parameters.get("verbose", False) self.drift_df = pd.DataFrame() self.drift_rebalancer_logic = DriftRebalancerLogic( strategy=self, @@ -108,9 +109,7 @@ def initialize(self, parameters: Any = None) -> None: # noinspection PyAttributeOutsideInit def on_trading_iteration(self) -> None: dt = self.get_datetime() - msg = f"{dt} on_trading_iteration called" - self.logger.info(msg) - self.log_message(msg, broadcast=True) + self.logger.info(f"{dt} on_trading_iteration called") self.cancel_open_orders() if self.cash < 0: @@ -120,22 +119,5 @@ def on_trading_iteration(self) -> None: ) self.drift_df = self.drift_rebalancer_logic.calculate(target_weights=self.target_weights) - rebalance_needed = self.drift_rebalancer_logic.rebalance(drift_df=self.drift_df) - - if rebalance_needed: - msg = f"Rebalancing portfolio." - self.logger.info(msg) - self.log_message(msg, broadcast=True) - - def on_abrupt_closing(self): - dt = self.get_datetime() - 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() - self.logger.info(f"{dt} on_bot_crash called") - self.log_message(f"Bot crashed with error: {error}", broadcast=True) - self.cancel_open_orders() - + self.drift_rebalancer_logic.rebalance(drift_df=self.drift_df) + \ No newline at end of file diff --git a/lumibot/strategies/_strategy.py b/lumibot/strategies/_strategy.py index 0b38137c2..93af0da47 100644 --- a/lumibot/strategies/_strategy.py +++ b/lumibot/strategies/_strategy.py @@ -215,6 +215,11 @@ def __init__( self.save_logfile = save_logfile self.broker = broker + # initialize cash variables + self._cash = None + self._position_value = None + self._portfolio_value = None + if name is not None: self._name = name diff --git a/lumibot/strategies/strategy.py b/lumibot/strategies/strategy.py index 6835a6346..27005f0ae 100644 --- a/lumibot/strategies/strategy.py +++ b/lumibot/strategies/strategy.py @@ -2885,6 +2885,16 @@ def get_historical_prices( >>> self.log_message(f"Last price of BTC in USD: {last_ohlc['close']}, and the open price was {last_ohlc['open']}") """ + # Get that length is type int and if not try to cast it + if not isinstance(length, int): + try: + length = int(length) + except Exception as e: + raise ValueError( + f"Invalid length parameter in get_historical_prices() method. Length must be an int but instead got {length}, " + f"which is a type {type(length)}." + ) + if quote is None: quote = self.quote_asset diff --git a/lumibot/tools/indicators.py b/lumibot/tools/indicators.py index f09ff91e5..04f475669 100644 --- a/lumibot/tools/indicators.py +++ b/lumibot/tools/indicators.py @@ -258,6 +258,9 @@ def generate_marker_plotly_text(row): marker_size = marker_df["size"].iloc[0] marker_size = marker_size if marker_size else 25 + # If color is not set, set it to black + marker_df.loc[:, "color"] = marker_df["color"].fillna("white") + # Create a new trace for this marker name fig.add_trace( go.Scatter( diff --git a/setup.py b/setup.py index 840bae035..def1653d3 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="lumibot", - version="3.8.14", + version="3.8.17", author="Robert Grzesik", author_email="rob@lumiwealth.com", description="Backtesting and Trading Library, Made by Lumiwealth",