diff --git a/.gitignore b/.gitignore index a900f0f19..d4b79f2d1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ todos.txt test_bot.py .vscode .coverage* -*secret*/**.env +.env lumi_tradier lumiwealth_tradier ThetaTerminal.jar diff --git a/README.md b/README.md index 03308d9e8..135ad7221 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,14 @@ Our blog has lots of example strategies and shows you how to run a bot using Lum **https://lumiwealth.com/blog/** +## Run a backtest + +To run a backtest, you can use the following code snippet: + +```bash +python -m lumibot.example_strategies.stock_buy_and_hold +``` + ## Run an Example Strategy We made a small example strategy to show you how to use Lumibot in this GitHub repository: [Example Algorithm GitHub](https://github.com/Lumiwealth-Strategies/stock_example_algo) diff --git a/lumibot/backtesting/backtesting_broker.py b/lumibot/backtesting/backtesting_broker.py index 5495eb634..82a6a8b37 100644 --- a/lumibot/backtesting/backtesting_broker.py +++ b/lumibot/backtesting/backtesting_broker.py @@ -12,7 +12,6 @@ from lumibot.trading_builtins import CustomStream logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) class BacktestingBroker(Broker): diff --git a/lumibot/credentials.py b/lumibot/credentials.py index 2ce9178c3..de4c14a93 100644 --- a/lumibot/credentials.py +++ b/lumibot/credentials.py @@ -16,7 +16,6 @@ # Configure logging logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) def find_and_load_dotenv(base_dir) -> bool: diff --git a/lumibot/data_sources/yahoo_data.py b/lumibot/data_sources/yahoo_data.py index 380e38584..43777e454 100644 --- a/lumibot/data_sources/yahoo_data.py +++ b/lumibot/data_sources/yahoo_data.py @@ -9,7 +9,6 @@ from lumibot.tools import YahooHelper logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) class YahooData(DataSourceBacktesting): diff --git a/lumibot/example_strategies/lifecycle_logger.py b/lumibot/example_strategies/lifecycle_logger.py index 3f1ceef1a..7306f60fe 100644 --- a/lumibot/example_strategies/lifecycle_logger.py +++ b/lumibot/example_strategies/lifecycle_logger.py @@ -1,9 +1,9 @@ import logging +import datetime from lumibot.strategies.strategy import Strategy logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) class LifecycleLogger(Strategy): @@ -37,3 +37,35 @@ def after_market_closes(self): dt = self.get_datetime() logger.info(f"{dt} after_market_closes called") + +if __name__ == "__main__": + IS_BACKTESTING = True + + if IS_BACKTESTING: + from lumibot.backtesting import YahooDataBacktesting + + # Backtest this strategy + backtesting_start = datetime.datetime(2023, 1, 1) + backtesting_end = datetime.datetime(2024, 9, 1) + + results = LifecycleLogger.backtest( + YahooDataBacktesting, + backtesting_start, + backtesting_end, + benchmark_asset="SPY", + # show_progress_bar=False, + # quiet_logs=False, + ) + + # Print the results + print(results) + else: + from lumibot.credentials import ALPACA_CONFIG + from lumibot.brokers import Alpaca + from lumibot.traders import Trader + + trader = Trader() + broker = Alpaca(ALPACA_CONFIG) + strategy = LifecycleLogger(broker=broker) + trader.add_strategy(strategy) + strategy_executors = trader.run_all() diff --git a/lumibot/strategies/_strategy.py b/lumibot/strategies/_strategy.py index 7947d3e35..38892fbef 100644 --- a/lumibot/strategies/_strategy.py +++ b/lumibot/strategies/_strategy.py @@ -1013,9 +1013,6 @@ def run_backtest( if not hasattr(self, "logger") or self.logger is None: self.logger = CustomLoggerAdapter(logger, {'strategy_name': self._name}) - # Print start message - print(f"Starting backtest for {datasource_class.__name__}...") - # If show_plot is None, then set it to True if show_plot is None: show_plot = SHOW_PLOT diff --git a/lumibot/tools/indicators.py b/lumibot/tools/indicators.py index 1a22d858d..f09ff91e5 100644 --- a/lumibot/tools/indicators.py +++ b/lumibot/tools/indicators.py @@ -19,7 +19,6 @@ from .yahoo_helper import YahooHelper as yh logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) def total_return(_df): diff --git a/lumibot/traders/trader.py b/lumibot/traders/trader.py index 0f5599abf..b079951b5 100644 --- a/lumibot/traders/trader.py +++ b/lumibot/traders/trader.py @@ -7,7 +7,6 @@ # Overloading time.sleep to warn users against using it logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) class Trader: @@ -142,6 +141,7 @@ def run_all( result = self._collect_analysis() if self.is_backtest_broker: + logger.setLevel(logging.INFO) logger.info("Backtesting finished") strat.backtest_analysis( logdir=self.logdir, @@ -189,8 +189,8 @@ def _set_logger(self): if self.debug: logger.setLevel(logging.DEBUG) - elif self.is_backtest_broker: - logger.setLevel(logging.INFO) + elif self.quiet_logs: + logger.setLevel(logging.ERROR) for handler in logger.handlers: if handler.__class__.__name__ == "StreamHandler": handler.setLevel(logging.ERROR) diff --git a/tests/backtest/fixtures.py b/tests/backtest/fixtures.py index f92f8ba2c..8301f7ece 100644 --- a/tests/backtest/fixtures.py +++ b/tests/backtest/fixtures.py @@ -10,7 +10,6 @@ from lumibot.backtesting import PolygonDataBacktesting logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) @pytest.fixture diff --git a/tests/backtest/test_example_strategies.py b/tests/backtest/test_example_strategies.py index 714e7cb54..930a640d7 100644 --- a/tests/backtest/test_example_strategies.py +++ b/tests/backtest/test_example_strategies.py @@ -41,7 +41,7 @@ def test_stock_bracket(self): show_plot=False, show_tearsheet=False, save_tearsheet=False, - polygon_api_key=POLYGON_API_KEY, + show_indicators=False, ) assert results assert isinstance(strat_obj, StockBracket) diff --git a/tests/backtest/test_pandas_backtest.py b/tests/backtest/test_pandas_backtest.py index 885eefa2f..e7de57808 100644 --- a/tests/backtest/test_pandas_backtest.py +++ b/tests/backtest/test_pandas_backtest.py @@ -8,7 +8,6 @@ logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) class TestPandasBacktest: diff --git a/tests/backtest/test_passing_trader_into_backtest.py b/tests/backtest/test_passing_trader_into_backtest.py index d91f24e6c..f7b6ccdef 100644 --- a/tests/backtest/test_passing_trader_into_backtest.py +++ b/tests/backtest/test_passing_trader_into_backtest.py @@ -9,7 +9,6 @@ from tests.backtest.fixtures import pandas_data_fixture logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) class TestPassingTraderIntoBacktest: diff --git a/tests/backtest/test_polygon.py b/tests/backtest/test_polygon.py index 8b2adb9b9..5a02ded31 100644 --- a/tests/backtest/test_polygon.py +++ b/tests/backtest/test_polygon.py @@ -6,6 +6,7 @@ import pandas_market_calendars as mcal + from tests.backtest.fixtures import polygon_data_backtesting import pytz from lumibot.backtesting import BacktestingBroker, PolygonDataBacktesting @@ -18,7 +19,7 @@ # Global parameters # API Key for testing Polygon.io -POLYGON_API_KEY = os.environ.get("POLYGON_API_KEY") +from lumibot.credentials import POLYGON_API_KEY class PolygonBacktestStrat(Strategy): diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 000000000..c335ff697 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,114 @@ +import datetime +import logging + +from lumibot.example_strategies.lifecycle_logger import LifecycleLogger +from lumibot.backtesting import YahooDataBacktesting + + +class TestLogging: + + def test_logging(self, caplog): + caplog.set_level(logging.INFO) + logger = logging.getLogger() + logger.info("This is an info message") + assert "This is an info message" in caplog.text + + def test_backtest_produces_no_logs_by_default(self, caplog): + caplog.set_level(logging.INFO) + backtesting_start = datetime.datetime(2023, 1, 2) + backtesting_end = datetime.datetime(2023, 1, 4) + + LifecycleLogger.backtest( + YahooDataBacktesting, + backtesting_start, + backtesting_end, + parameters={"sleeptime": "1D", "market": "NYSE"}, + show_plot=False, + save_tearsheet=False, + show_tearsheet=False, + show_indicators=False, + save_logfile=False, + ) + # count that this contains 3 new lines. Its an easy proxy for the number of log messages and avoids + # the issue where the datetime is always gonna be different. + assert caplog.text.count("\n") == 3 + assert "Starting backtest...\n" in caplog.text + assert "Backtesting starting...\n" in caplog.text + assert "Backtesting finished\n" in caplog.text + + def test_run_backtest_produces_no_logs_by_default(self, caplog): + caplog.set_level(logging.INFO) + backtesting_start = datetime.datetime(2023, 1, 2) + backtesting_end = datetime.datetime(2023, 1, 4) + + LifecycleLogger.run_backtest( + YahooDataBacktesting, + backtesting_start, + backtesting_end, + parameters={"sleeptime": "1D", "market": "NYSE"}, + show_plot=False, + save_tearsheet=False, + show_tearsheet=False, + show_indicators=False, + save_logfile=False, + ) + # count that this contains 3 new lines. Its an easy proxy for the number of log messages and avoids + # the issue where the datetime is always gonna be different. + assert caplog.text.count("\n") == 3 + assert "Starting backtest...\n" in caplog.text + assert "Backtesting starting...\n" in caplog.text + assert "Backtesting finished\n" in caplog.text + + def test_backtest_produces_no_logs_when_quiet_logs_is_true(self, caplog): + caplog.set_level(logging.INFO) + backtesting_start = datetime.datetime(2023, 1, 2) + backtesting_end = datetime.datetime(2023, 1, 4) + + LifecycleLogger.backtest( + YahooDataBacktesting, + backtesting_start, + backtesting_end, + parameters={"sleeptime": "1D", "market": "NYSE"}, + show_plot=False, + save_tearsheet=False, + show_tearsheet=False, + show_indicators=False, + save_logfile=False, + show_progress_bar=True, + quiet_logs=True, + ) + # count that this contains 3 new lines. Its an easy proxy for the number of log messages and avoids + # the issue where the datetime is always gonna be different. + assert caplog.text.count("\n") == 3 + assert "Starting backtest...\n" in caplog.text + assert "Backtesting starting...\n" in caplog.text + assert "Backtesting finished\n" in caplog.text + + def test_backtest_produces_logs_when_quiet_logs_is_false(self, caplog): + caplog.set_level(logging.INFO) + backtesting_start = datetime.datetime(2023, 1, 2) + backtesting_end = datetime.datetime(2023, 1, 4) + + LifecycleLogger.backtest( + YahooDataBacktesting, + backtesting_start, + backtesting_end, + parameters={"sleeptime": "1D", "market": "NYSE"}, + show_plot=False, + save_tearsheet=False, + show_tearsheet=False, + show_indicators=False, + save_logfile=False, + show_progress_bar=False, + quiet_logs=False, + ) + + assert caplog.text.count("\n") == 9 + assert "Starting backtest...\n" in caplog.text + assert "Backtesting starting...\n" in caplog.text + assert "before_market_opens called\n" in caplog.text + assert "before_starting_trading called\n" in caplog.text + assert "on_trading_iteration called\n" in caplog.text + assert "before_market_closes called\n" in caplog.text + assert "after_market_closes called\n" in caplog.text + assert "Backtesting finished\n" in caplog.text \ No newline at end of file