Lumiwealth / lumibot

Backtesting and Trading Bots Made Easy for Crypto, Stocks, Options, Futures, FOREX and more
GNU General Public License v3.0
922 stars 175 forks source link

lumibot example_strategies stock_momentum.py #630

Open pabloadmin opened 3 days ago

pabloadmin commented 3 days ago

Hi,

I tried this example and it didn't work initially, but I fixed it. I'm sharing it with you. Lumibot is a great tool.

Thanks!

from datetime import datetime

from lumibot.strategies.strategy import Strategy

""" Strategy Description

Buys the best performing asset from self.symbols over self.period number of days. For example, if SPY increased 2% yesterday, but VEU and AGG only increased 1% yesterday, then we will buy SPY. """

class Momentum(Strategy):

=====Overloading lifecycle methods=============

def initialize(self, symbols=None):
    # Setting the waiting period (in days)
    self.period = 2

    # The counter for the number of days we have been holding the current asset
    self.counter = 0

    # There is only one trading operation per day
    # No need to sleep between iterations
    self.sleeptime = 0

    # Set the symbols that we will be monitoring for momentum
    if symbols:
        self.symbols = symbols
    else:
        self.symbols = ["SPY", "VEU", "AGG"]

    # The asset that we want to buy/currently own, and the quantity
    self.asset = ""
    self.quantity = 0

def on_trading_iteration(self):
    # When the counter reaches the desired holding period,
    # re-evaluate which asset we should be holding
    momentums = []
    if self.counter == self.period or self.counter == 0:
        self.counter = 0
        momentums = self.get_assets_momentums()

        # Get the asset with the highest return in our period
        # (aka the highest momentum)
        momentums.sort(key=lambda x: x.get("return"))
        best_asset_data = momentums[-1]
        best_asset = best_asset_data["symbol"]
        best_asset_return = best_asset_data["return"]

        # Get the data for the currently held asset
        if self.asset:
            current_asset_data = [
                m for m in momentums if m["symbol"] == self.asset
            ][0]
            current_asset_return = current_asset_data["return"]

            # If the returns are equals, keep the current asset
            if current_asset_return >= best_asset_return:
                best_asset = self.asset
                best_asset_data = current_asset_data

        self.log_message("%s best symbol." % best_asset)

        # If the asset with the highest momentum has changed, buy the new asset
        if best_asset != self.asset:
            # Sell the current asset that we own
            if self.asset:
                self.log_message("Swapping %s for %s." % (self.asset, best_asset))
                order = self.create_order(self.asset, self.quantity, "sell")
                self.submit_order(order)

            # Calculate the quantity and send the buy order for the new asset
            self.asset = best_asset
            best_asset_price = best_asset_data["price"]
            self.quantity = int(self.portfolio_value // best_asset_price)
            order = self.create_order(self.asset, self.quantity, "buy")
            self.submit_order(order)
        else:
            self.log_message("Keeping %d shares of %s" % (self.quantity, self.asset))

    self.counter += 1

    # Stop for the day, since we are looking at daily momentums
    self.await_market_to_close()

def on_abrupt_closing(self):
    # Sell all positions
    self.sell_all()

def trace_stats(self, context, snapshot_before):
    """
    Add additional stats to the CSV logfile
    """
    # Get the values of all our variables from the last iteration
    row = {
        "old_best_asset": snapshot_before.get("asset"),
        "old_asset_quantity": snapshot_before.get("quantity"),
        "old_cash": snapshot_before.get("cash"),
        "new_best_asset": self.asset,
        "new_asset_quantity": self.quantity,
    }

    # Get the momentums of all the assets from the context of on_trading_iteration
    # (notice that on_trading_iteration has a variable called momentums, this is what
    # we are reading here)
    momentums = context.get("momentums")
    if len(momentums) != 0:
        for item in momentums:
            symbol = item.get("symbol")
            for key in item:
                if key != "symbol":
                    row[f"{symbol}_{key}"] = item[key]

    # Add all of our values to the row in the CSV file. These automatically get
    # added to portfolio_value, cash and return
    return row

# =============Helper methods====================

def get_assets_momentums(self):
    """
    Gets the momentums (the percentage return) for all the assets we are tracking,
    over the time period set in self.period
    """
    momentums = []
    data = self.get_historical_prices_for_assets(self.symbols, self.period + 2, timestep="day")
    # data = self.get_bars(self.symbols, self.period + 2, timestep="day")
    for asset, bars_set in data.items():
        # Get the return for symbol over self.period days
        symbol = asset.symbol
        symbol_momentum = bars_set.get_momentum(end=self.period)
        self.log_message(
            "%s has a return value of %.2f%% over the last %d day(s)."
            % (symbol, 100 * symbol_momentum, self.period)
        )

        momentums.append(
            {
                "symbol": symbol,
                "price": bars_set.get_last_price(),
                "return": symbol_momentum,
            }
        )

    return momentums

if name == "main": is_live = 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 = Momentum(broker=broker)

    trader.add_strategy(strategy)
    strategy_executors = trader.run_all()

else:
    from lumibot.backtesting import YahooDataBacktesting

    # Backtest this strategy
    backtesting_start = datetime(2023, 1, 1)
    backtesting_end = datetime(2024, 8, 1)

    results = Momentum.backtest(
        YahooDataBacktesting,
        backtesting_start,
        backtesting_end,
        benchmark_asset="SPY",
        logfile="./log.txt",
    )

#############################################################

requirements.txt

aiodns==3.2.0 aiohappyeyeballs==2.4.3 aiohttp==3.11.6 aiosignal==1.3.1 alpaca-py==0.33.0 alpha_vantage==3.0.0 annotated-types==0.7.0 appdirs==1.4.4 APScheduler==3.10.4 asttokens==2.4.1 async-timeout==5.0.1 attrs==24.2.0 bcrypt==4.2.1 beautifulsoup4==4.12.3 bidict==0.23.1 blinker==1.9.0 ccxt==4.2.85 certifi==2024.8.30 cffi==1.15.0 charset-normalizer==3.4.0 click==8.1.7 colorama==0.4.6 contourpy==1.3.0 cryptography==43.0.3 cycler==0.12.1 decorator==5.1.1 dnspython==2.7.0 duckdb==1.1.3 email_validator==2.2.0 exceptiongroup==1.2.2 exchange_calendars==4.5.6 executing==2.1.0 Flask==3.1.0 Flask-Login==0.6.3 flask-marshmallow==1.2.1 Flask-Principal==0.4.0 Flask-Security==5.5.2 Flask-SocketIO==5.4.1 Flask-SQLAlchemy==3.1.1 Flask-WTF==1.2.2 fonttools==4.55.0 frozendict==2.4.6 frozenlist==1.5.0 greenlet==3.1.1 h11==0.14.0 holidays==0.53 html5lib==1.1 ibapi==9.81.1.post1 idna==3.10 ijson==3.3.0 importlib_metadata==8.5.0 importlib_resources==6.4.5 inflection==0.5.1 iniconfig==2.0.0 ipython==8.29.0 itsdangerous==2.2.0 jedi==0.19.2 Jinja2==3.1.4 jsonpickle==4.0.0 kiwisolver==1.4.6 korean-lunar-calendar==0.3.1 lumibot==3.3.1 lumiwealth-tradier==0.1.12 lxml==5.3.0 MarkupSafe==3.0.2 marshmallow==3.23.1 marshmallow-sqlalchemy==1.1.0 matplotlib==3.9.2 matplotlib-inline==0.1.7 more-itertools==10.5.0 msgpack==1.1.0 multidict==6.1.0 multitasking==0.0.11 numpy==1.26.4 packaging==24.2 pandas==2.2.2 pandas-datareader==0.10.0 pandas_market_calendars==4.4.2 parso==0.8.4 passlib==1.7.4 peewee==3.17.8 pillow==10.4.0 platformdirs==4.3.6 plotly==5.24.1 pluggy==1.5.0 polygon-api-client==1.14.2 prompt_toolkit==3.0.48 propcache==0.2.0 psutil==6.1.0 psycopg2-binary==2.9.10 pure_eval==0.2.3 pyarrow==18.0.0 pycares==4.4.0 pycparser==2.22 pydantic==2.0.3 pydantic_core==2.3.0 Pygments==2.18.0 pyluach==2.2.0 pyOpenSSL==22.0.0 pyopenssl-sdk==0.0.0.post0 pyparsing==3.2.0 pytest==8.3.3 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 python-engineio==4.10.1 python-socketio==5.11.4 pytz==2024.2 Quandl==3.7.0 quantstats-lumi==0.3.3 requests==2.32.3 scipy==1.10.1 seaborn==0.13.2 simple-websocket==1.1.0 six==1.16.0 soupsieve==2.6 SQLAlchemy==2.0.36 sseclient-py==1.8.0 stack-data==0.6.3 tabulate==0.9.0 tenacity==9.0.0 termcolor==2.5.0 thetadata==0.9.11 tomli==2.1.0 toolz==1.0.0 tqdm==4.67.0 traitlets==5.14.3 typing_extensions==4.12.2 tzdata==2024.2 tzlocal==5.2 urllib3==2.2.3 uuid==1.30 wcwidth==0.2.13 webencodings==0.5.1 websocket-client==1.8.0 websockets==12.0 Werkzeug==3.1.3 wget==3.2 wsproto==1.2.0 WTForms==3.2.1 yarl==1.17.2 yfinance==0.2.50 zipp==3.21.0

grzesir commented 3 days ago

hey thanks @pabloadmin ! so what exactly was the issue? and how were you running it to get the problem? it works fine in render and on my local computer

pabloadmin commented 2 days ago

Hi, i used the requirements.txt that appear in root github repository to install all libraries, but the code didn't work. Maybe was a differents versions of libraries, different functions parameters, at this line made a change in the nema of parameter:

symbol_momentum = bars_set.get_momentum(num_periods=self.period)

symbol_momentum = bars_set.get_momentum(end=self.period)

So that i shared the requirements.txt that i used.

thanks to response!

brettelliot commented 1 day ago

This is the correct (new) way to call get_momentum:

symbol_momentum = bars_set.get_momentum(num_periods=self.period)

If that doesn't work but using end does, then you are using an older version of Lumibot. The signature of get_momentum changed in 3.8.2.

Can you confirm what version you tried?