Skip to content

Commit

Permalink
Merged upstream and resolved conflict.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jim White committed Mar 11, 2024
2 parents d0f2fc3 + 26a19f4 commit 957b69c
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 59 deletions.
2 changes: 1 addition & 1 deletion lumibot/data_sources/pandas_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def get_asset_by_symbol(self, symbol, asset_type=None):
list of Asset
"""
store_assets = self.get_assets()
if type is None:
if asset_type is None:
return [asset for asset in store_assets if asset.symbol == symbol]
else:
return [asset for asset in store_assets if (asset.symbol == symbol and asset.asset_type == asset_type)]
Expand Down
38 changes: 28 additions & 10 deletions lumibot/strategies/_strategy.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import datetime
import logging
import os
import warnings
from asyncio.log import logger
from decimal import Decimal

import jsonpickle
import pandas as pd

from lumibot.backtesting import BacktestingBroker, PolygonDataBacktesting
from lumibot.entities import Asset, Position
from lumibot.tools import (create_tearsheet, day_deduplicate,
get_risk_free_rate, get_symbol_returns,
plot_indicators, plot_returns, stats_summary,
to_datetime_aware)
from lumibot.tools import (
create_tearsheet,
day_deduplicate,
get_symbol_returns,
plot_indicators,
plot_returns,
stats_summary,
to_datetime_aware,
)
from lumibot.traders import Trader

from .strategy_executor import StrategyExecutor


class CustomLoggerAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
# Use an f-string for formatting
return f'[{self.extra["strategy_name"]}] {msg}', kwargs

class _Strategy:
IS_BACKTESTABLE = True

Expand Down Expand Up @@ -45,6 +54,7 @@ def __init__(
discord_webhook_url=None,
account_history_db_connection_str=None,
strategy_id=None,
discord_account_summary_footer=None,
**kwargs,
):
"""Initializes a Strategy object.
Expand Down Expand Up @@ -107,6 +117,10 @@ def __init__(
must be set for this to work). Defaults to None (no discord alerts).
For instructions on how to create a discord webhook url, see this link:
https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks
discord_account_summary_footer : str
The footer to use for the account summary sent to the discord channel if discord_webhook_url is set and the
account_history_db_connection_str is set.
Defaults to None (no footer).
account_history_db_connection_str : str
The connection string to use for the account history database. This is used to store the account history
for the strategy. The account history is sent to the discord channel at the end of each day. The connection
Expand Down Expand Up @@ -158,8 +172,12 @@ def __init__(
if self._name is None:
self._name = self.__class__.__name__

# Create an adapter with 'strategy_name' set to the instance's name
self.logger = CustomLoggerAdapter(logger, {'strategy_name': self._name})

self.discord_webhook_url = discord_webhook_url
self.account_history_db_connection_str = account_history_db_connection_str
self.discord_account_summary_footer = discord_account_summary_footer

if strategy_id is None:
self.strategy_id = self._name
Expand Down Expand Up @@ -290,7 +308,7 @@ def _copy_dict(self):
result[key] = self.__dict__[key]
except KeyError:
pass
# logging.warning(
# self.logger.warning(
# "Cannot perform deepcopy on %r" % self.__dict__[key]
# )
elif key in [
Expand Down Expand Up @@ -637,9 +655,9 @@ def plot_returns_vs_benchmark(
if not show_plot:
return
elif self._strategy_returns_df is None:
logging.warning("Cannot plot returns because the strategy returns are missing")
self.logger.warning("Cannot plot returns because the strategy returns are missing")
elif self._benchmark_returns_df is None:
logging.warning("Cannot plot returns because the benchmark returns are missing")
self.logger.warning("Cannot plot returns because the benchmark returns are missing")
else:
plot_returns(
self._strategy_returns_df,
Expand All @@ -665,7 +683,7 @@ def tearsheet(
save_tearsheet = True

if self._strategy_returns_df is None:
logging.warning("Cannot create a tearsheet because the strategy returns are missing")
self.logger.warning("Cannot create a tearsheet because the strategy returns are missing")
else:
strat_name = self._name if self._name is not None else "Strategy"
create_tearsheet(
Expand Down
32 changes: 16 additions & 16 deletions lumibot/strategies/strategy.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import datetime
import io
import logging
import os
import time
import uuid
Expand Down Expand Up @@ -30,7 +29,6 @@
# Set the stats table name for when storing stats in a database, defined by account_history_db_connection_str
STATS_TABLE_NAME = "strategy_tracker"


class Strategy(_Strategy):
@property
def name(self):
Expand Down Expand Up @@ -366,12 +364,10 @@ def log_message(self, message, color=None, broadcast=False):
>>> self.log_message('Sending a buy order')
"""
if color is not None:
colored_message = f"{self._log_strat_name()}: {message}"
colored_message = colored(message, color)
logging.info(colored_message)
self.logger.info(colored_message)
else:
output_message = f"{self._log_strat_name()}: {message}"
logging.info(output_message)
self.logger.info(message)

if broadcast:
# Send the message to Discord
Expand Down Expand Up @@ -1490,7 +1486,7 @@ def submit_order(self, order):
"""

if order is None:
logging.error(
self.logger.error(
"Cannot submit a None order, please check to make sure that you have actually created an order before submitting."
)
return
Expand Down Expand Up @@ -1860,7 +1856,7 @@ def get_last_price(self, asset, quote=None, exchange=None, should_use_last_close

# Check if the asset is valid
if asset is None or (isinstance(asset, Asset) and not asset.is_valid()):
logging.error(
self.logger.error(
f"Asset in get_last_price() must be a valid asset. Got {asset} of type {type(asset)}. You may be missing some of the required parameters for the asset type (eg. strike price for options, expiry for options/futures, etc)."
)
return None
Expand Down Expand Up @@ -2919,7 +2915,7 @@ def get_historical_prices(
if quote is None:
quote = self.quote_asset

logging.info(f"Getting historical prices for {asset}, {length} bars, {timestep}")
self.logger.info(f"Getting historical prices for {asset}, {length} bars, {timestep}")

asset = self._sanitize_user_asset(asset)

Expand Down Expand Up @@ -3032,7 +3028,7 @@ def get_historical_prices_for_assets(
>>> df = bars.df
"""

logging.info(f"Getting historical prices for {assets}, {length} bars, {timestep}")
self.logger.info(f"Getting historical prices for {assets}, {length} bars, {timestep}")

assets = [self._sanitize_user_asset(asset) for asset in assets]
return self.broker.data_source.get_bars(
Expand Down Expand Up @@ -3683,13 +3679,13 @@ def send_discord_message(self, message, image_buf=None, silent=True):
# Check if the message is empty
if message == "" or message is None:
# If the message is empty, log and return
logging.debug("The discord message is empty. Please provide a message to send to Discord.")
self.logger.debug("The discord message is empty. Please provide a message to send to Discord.")
return

# Check if the discord webhook URL is set
if self.discord_webhook_url is None:
# If the webhook URL is not set, log and return
logging.debug(
self.logger.debug(
"The discord webhook URL is not set. Please set the discord_webhook_url parameter in the strategy \
initialization if you want to send messages to Discord."
)
Expand Down Expand Up @@ -3721,10 +3717,10 @@ def send_discord_message(self, message, image_buf=None, silent=True):

# Check if the message was sent successfully
if response.status_code == 200 or response.status_code == 204:
print("Discord message sent successfully.")
self.logger.info("Discord message sent successfully.")
else:
print(
f"ERROR: Failed to send message to Discord. Status code: {response.status_code}, message: {response.text}"
self.logger.error(
f"Failed to send message to Discord. Status code: {response.status_code}, message: {response.text}"
)

def send_spark_chart_to_discord(self, stats_df, portfolio_value, now):
Expand Down Expand Up @@ -3843,7 +3839,7 @@ def send_result_text_to_discord(self, returns_text, portfolio_value, cash):

# Make sure last_price is a number
if last_price is None or not isinstance(last_price, (int, float, Decimal)):
logging.info(f"Last price for {position.asset} is not a number: {last_price}")
self.logger.info(f"Last price for {position.asset} is not a number: {last_price}")
continue

# Calculate teh value of the position
Expand Down Expand Up @@ -3891,6 +3887,10 @@ def send_result_text_to_discord(self, returns_text, portfolio_value, cash):
# Remove the extra spaces at the beginning of each line
message = "\n".join(line.lstrip() for line in message.split("\n"))

# Add self.discord_account_summary_footer to the message
if hasattr(self, "discord_account_summary_footer") and self.discord_account_summary_footer is not None:
message += f"{self.discord_account_summary_footer}\n\n"

# Add powered by Lumiwealth to the message
message += "[**Powered by 💡 Lumiwealth**](<https://lumiwealth.com>)\n-----------"

Expand Down
72 changes: 41 additions & 31 deletions lumibot/strategies/strategy_executor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import inspect
import logging
import time
import traceback
from datetime import datetime, timedelta
Expand Down Expand Up @@ -105,11 +104,11 @@ def sync_broker(self):
# Snapshot for the broker and lumibot:
cash_broker = self.broker._get_balances_at_broker(self.strategy.quote_asset)
if cash_broker is None and cash_broker_retries < cash_broker_max_retries:
logging.info("Unable to get cash from broker, trying again.")
self.strategy.logger.info("Unable to get cash from broker, trying again.")
cash_broker_retries += 1
continue
elif cash_broker is None and cash_broker_retries >= cash_broker_max_retries:
logging.info(
self.strategy.logger.info(
f"Unable to get the cash balance after {cash_broker_max_retries} "
f"tries, setting cash to zero."
)
Expand Down Expand Up @@ -157,7 +156,7 @@ def sync_broker(self):
obroker = getattr(order, order_attr)
if olumi != obroker:
setattr(order_lumi, order_attr, obroker)
logging.warning(
self.strategy.logger.warning(
f"We are adjusting the {order_attr} of the order {order_lumi}, from {olumi} "
f"to be {obroker} because what we have in memory does not match the broker."
)
Expand All @@ -172,7 +171,7 @@ def sync_broker(self):
# However, active orders should not be dropped as they are still in effect and if they can't
# be found in the broker, they should be canceled because something went wrong.
if order_lumi.is_active():
logging.info(
self.strategy.logger.info(
f"Cannot find order {order_lumi} (id={order_lumi.identifier}) in broker "
f"(bkr cnt={len(orders_broker)}), canceling."
)
Expand Down Expand Up @@ -342,30 +341,41 @@ def _on_trading_iteration(self):
on_trading_iteration = append_locals(self.strategy.on_trading_iteration)

# Time-consuming
on_trading_iteration()
try:
on_trading_iteration()

self.strategy._first_iteration = False
self._strategy_context = on_trading_iteration.locals
self.strategy._last_on_trading_iteration_datetime = datetime.now()
self.process_queue()
self.strategy._first_iteration = False
self._strategy_context = on_trading_iteration.locals
self.strategy._last_on_trading_iteration_datetime = datetime.now()
self.process_queue()

end_dt = datetime.now()
end_str = end_dt.strftime("%Y-%m-%d %H:%M:%S")
runtime = (end_dt - start_dt).total_seconds()

end_dt = datetime.now()
end_str = end_dt.strftime("%Y-%m-%d %H:%M:%S")
runtime = (end_dt - start_dt).total_seconds()

# Update cron count to account for how long this iteration took to complete so that the next iteration will
# occur at the correct time.
self.cron_count = self._seconds_to_sleeptime_count(int(runtime), sleep_units)
next_run_time = self.get_next_ap_scheduler_run_time()
if next_run_time is not None:
# Format the date to be used in the log message.
dt_str = next_run_time.strftime("%Y-%m-%d %H:%M:%S")
# Update cron count to account for how long this iteration took to complete so that the next iteration will
# occur at the correct time.
self.cron_count = self._seconds_to_sleeptime_count(int(runtime), sleep_units)
next_run_time = self.get_next_ap_scheduler_run_time()
if next_run_time is not None:
# Format the date to be used in the log message.
dt_str = next_run_time.strftime("%Y-%m-%d %H:%M:%S")
self.strategy.log_message(
f"Trading iteration ended at {end_str}, next check in time is {dt_str}. Took {runtime:.2f}s", color="blue"
)

else:
self.strategy.log_message(f"Trading iteration ended at {end_str}", color="blue")
except Exception as e:
# Log the error
self.strategy.log_message(
f"Trading iteration ended at {end_str}, next check in time is {dt_str}. Took {runtime:.2f}s", color="blue"
f"An error occurred during the on_trading_iteration lifecycle method: {e}", color="red"
)

else:
self.strategy.log_message(f"Trading iteration ended at {end_str}", color="blue")
# Log the traceback
self.strategy.log_message(traceback.format_exc(), color="red")

self._on_bot_crash(e)

@lifecycle_method
def _before_market_closes(self):
Expand Down Expand Up @@ -612,7 +622,7 @@ def calculate_strategy_trigger(self, force_start_immediately=False):
# Second with 0 in front if less than 10
kwargs["second"] = f"0{second}" if second < 10 else str(second)

logging.warning(
self.strategy.logger.warning(
f"The strategy will run at {kwargs['hour']}:{kwargs['minute']}:{kwargs['second']} every day. "
f"If instead you want to start right now and run every {time_raw} days then set "
f"force_start_immediately=True in the strategy's initialization."
Expand Down Expand Up @@ -877,13 +887,13 @@ def run(self):
self._run_trading_session()
except Exception as e:
# The bot crashed so log the error, call the on_bot_crash method, and continue
logging.error(e)
logging.error(traceback.format_exc())
self.strategy.logger.error(e)
self.strategy.logger.error(traceback.format_exc())
try:
self._on_bot_crash(e)
except Exception as e1:
logging.error(e1)
logging.error(traceback.format_exc())
self.strategy.logger.error(e1)
self.strategy.logger.error(traceback.format_exc())

# In BackTesting, we want to stop the bot if it crashes so there isn't an infinite loop
if self.strategy.is_backtesting:
Expand All @@ -897,8 +907,8 @@ def run(self):
try:
self._on_strategy_end()
except Exception as e:
logging.error(e)
logging.error(traceback.format_exc())
self.strategy.logger.error(e)
self.strategy.logger.error(traceback.format_exc())
self._on_bot_crash(e)
self.result = self.strategy._analysis
return False
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.2.0",
version="3.2.1",
author="Robert Grzesik",
author_email="rob@lumiwealth.com",
description="Backtesting and Trading Library, Made by Lumiwealth",
Expand Down

0 comments on commit 957b69c

Please sign in to comment.