Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/dev' into m-dev
Browse files Browse the repository at this point in the history
  • Loading branch information
Al4ise committed Dec 9, 2024
2 parents 7f52f76 + 9239262 commit ff43148
Show file tree
Hide file tree
Showing 12 changed files with 87 additions and 79 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lumibot/brokers/interactive_brokers_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
68 changes: 30 additions & 38 deletions lumibot/components/drift_rebalancer_logic.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from abc import ABC, abstractmethod
from typing import Dict, Any
from decimal import Decimal, ROUND_DOWN
import time
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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(),
Expand Down Expand Up @@ -229,15 +230,15 @@ 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):
# Should we short everything we have
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.
Expand Down Expand Up @@ -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)
Expand All @@ -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 = []
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
4 changes: 2 additions & 2 deletions lumibot/data_sources/interactive_brokers_rest_data.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -1078,4 +1078,4 @@ def get_quote(self, asset, quote=None, exchange=None):
else:
result["ask"] = None

return result
return result
9 changes: 8 additions & 1 deletion lumibot/data_sources/pandas_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
32 changes: 19 additions & 13 deletions lumibot/data_sources/tradier_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
2 changes: 1 addition & 1 deletion lumibot/entities/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 4 additions & 22 deletions lumibot/example_strategies/drift_rebalancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
Expand All @@ -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)

5 changes: 5 additions & 0 deletions lumibot/strategies/_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions lumibot/strategies/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions lumibot/tools/indicators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="lumibot",
version="3.8.14",
version="3.8.17",
author="Robert Grzesik",
author_email="[email protected]",
description="Backtesting and Trading Library, Made by Lumiwealth",
Expand Down

0 comments on commit ff43148

Please sign in to comment.