1

There is a pickle file that has many (say 10) stocks names and also have a folder that has all stock data I am trying to run the code to do the MACD analysis and hope can write down the result buy and sell time, price, position, cash on hand. when I have gone this far and trying to run MACD on all stocks. but after two loops, the data load is wrong. and repeats the results.

'''
import backtrader as bt
import pandas as pd
import pickle
import math


def run_test_macd():
    with open("sp500tickers.pickle", "rb") as f:
        tickers = pickle.load(f)
    cerebro.addstrategy(GoldenCross.gold_cross_class)
    for ticker in tickers:
        macd_stock_test2(ticker)

def macd_stock_test2(ticker):
    # print("stock test")
    # print("ticker")
    cerebro.broker.set_cash(1000000)
    ticker_prices = pd.read_csv('stock_dfs/{}.csv'.format(ticker), index_col='Date', parse_dates=True)

    # print(ticker_prices)
    # ticker prices
    feed = bt.feeds.PandasData(dataname=ticker_prices)
    print(feed)
    print(ticker)
    cerebro.adddata(feed)

    # cerebro.addsizer(bt.sizers.FixedSize, stake=1000)
    # cerebro.addanalyzer(btanalyzers.DrawDown, _name='drawdown')
    # cerebro.addanalyzer(btanalyzers.DrawDown, _name='returns')

    print('starting protfolio value: %.2f' % cerebro.broker.getvalue())
    cerebro.run()

    print('final protfolio value: %.2f' % cerebro.broker.getvalue())
    # cerebro.addanalyzer(SQN)
    #
    # cerebro.addwriter(bt.WriterFile, csv=args.writercsv, rounding=2)
    # cerebro.plot(style='candle')

class gold_cross_class(bt.Strategy):

    #set parameters to define fast and slow
    params = (('fast',40),('slow',150),('order_percentage',0.99),('ticker', "stock"))

    #define constractors
    def __init__(self):
        print("position size:",self.position.size)

        self.fast_moving_average=bt.indicators.EMA(
            self.data.close, period=self.params.fast, plotname='40 day moving average'
        )

        self.slow_moving_average = bt.indicators.EMA(
            self.data.close,  period=self.params.slow, plotname='150 day moving average'
        )

        self.crossover = bt.indicators.CrossOver(self.fast_moving_average, self.slow_moving_average)

    def next(self):
        if self.position.size == 0:
            if self.crossover >0:
                amount_to_invest = (self.params.order_percentage *self.broker.cash)
                self.size=math.floor(amount_to_invest/self.data.close)

                print("Buy {} shares of {} at {} on {}".format(self.size,self.params.ticker, self.data.close[0],self.data.close[0]))
                self.buy(size=self.size)

        if self.position.size > 0:
            if self.crossover<0:
                print("Sell {} shares of {} at {}".format(self.size,self.params.ticker, self.data.close[0]))
                self.sell(size=self.size)
'''

buy and sell price issue

buy order issue when 100% cash is used

updated buy & sell price issues

DMG
  • 27
  • 1
  • 7
  • I want to display the buy date '''print("Buy {} shares of {} at {} on {}".format(self.size,self.params.ticker, self.data.close[0],self.data.Date[0]))''' there is an error message: ' AttributeError: 'Lines_LineSeries_DataSeries_OHLC_OHLCDateTime_Abst' object has no attribute 'Date'' – DMG Apr 07 '21 at 20:26
  • 1
    Use this for the date: self.data.datetime[0] – run-out Apr 07 '21 at 23:55

1 Answers1

0

See my comments in the docstring at the head of the code. I have commented out some of your data loading only so I can load my own data, since I don't have your data.

import datetime
import backtrader as bt
import pandas as pd
import pickle
import math

"""
You have a number of issues. 
1. Establish cerebro in `macd_stock_test2` 
2. Call the `run_test_macd` from if __name__ == "__main__":
3. Add in print log
4. Add in order and trade notify.
5. Don't pass in the strategy, create it in `macd_stock_test2`.
6. Change name of GoldCross class to python standard.

"""


def run_test_macd():
    # with open("sp500tickers.pickle", "rb") as f:
    #     tickers = pickle.load(f)
    tickers = ["TSLA", "FB"]
    for ticker in tickers:
        macd_stock_test2(ticker)


def macd_stock_test2(ticker):
    # print("stock test")
    # print("ticker")
    cerebro = bt.Cerebro()
    cerebro.addstrategy(GoldCross)

    cerebro.broker.set_cash(1000000)
    # ticker_prices = pd.read_csv(
    #     "stock_dfs/{}.csv".format(ticker), index_col="Date", parse_dates=True
    # )

    # print(ticker_prices)
    # ticker prices
    # feed = bt.feeds.PandasData(dataname=ticker_prices)
    # print(feed)
    # print(ticker)
    feed = bt.feeds.YahooFinanceData(
        dataname=ticker,
        timeframe=bt.TimeFrame.Days,
        fromdate=datetime.datetime(2019, 1, 1),
        todate=datetime.datetime(2020, 12, 31),
        reverse=False,
    )
    cerebro.adddata(feed)

    # cerebro.addsizer(bt.sizers.FixedSize, stake=1000)
    # cerebro.addanalyzer(btanalyzers.DrawDown, _name='drawdown')
    # cerebro.addanalyzer(btanalyzers.DrawDown, _name='returns')

    print("starting portfolio value: %.2f" % cerebro.broker.getvalue())
    cerebro.run()

    print("final portfolio value: %.2f" % cerebro.broker.getvalue())
    # cerebro.addanalyzer(SQN)
    #
    # cerebro.addwriter(bt.WriterFile, csv=args.writercsv, rounding=2)
    # cerebro.plot(style='candle')


class GoldCross(bt.Strategy):

    # set parameters to define fast and slow
    params = (
        ("fast", 40),
        ("slow", 150),
        ("order_percentage", 0.99),
        ("ticker", "stock"),
    )

    # define constractors
    def __init__(self):
        print("position size:", self.position.size)

        self.fast_moving_average = bt.indicators.EMA(
            self.data.close, period=self.params.fast, plotname="40 day moving average"
        )

        self.slow_moving_average = bt.indicators.EMA(
            self.data.close, period=self.params.slow, plotname="150 day moving average"
        )

        self.crossover = bt.indicators.CrossOver(
            self.fast_moving_average, self.slow_moving_average
        )

    def log(self, txt, dt=None):
        """ Logging function fot this strategy"""
        dt = dt or self.data.datetime[0]
        if isinstance(dt, float):
            dt = bt.num2date(dt)
        print("%s, %s" % (dt.date(), txt))

    def notify_order(self, order):
        """ Triggered upon changes to orders. """

        # Suppress notification if it is just a submitted order.
        if order.status == order.Submitted:
            return

        # Print out the date, security name, order number and status.
        dt, dn = self.datetime.date(), order.data._name
        type = "Buy" if order.isbuy() else "Sell"
        self.log(
            f"{order.data._name:<6} Order: {order.ref:3d}\tType: {type:<5}\tStatus"
            f" {order.getstatusname():<8} \t"
            f"Size: {order.created.size:9.4f} Price: {order.created.price:9.4f} "
            f"Position: {self.getposition(order.data).size}"
        )
        if order.status == order.Margin:
            return

        # Check if an order has been completed
        if order.status in [order.Completed]:
            self.log(
                f"{order.data._name:<6} {('BUY' if order.isbuy() else 'SELL'):<5} "
                # f"EXECUTED for: {dn} "
                f"Price: {order.executed.price:6.2f} "
                f"Cost: {order.executed.value:6.2f} "
                f"Comm: {order.executed.comm:4.2f} "
                f"Size: {order.created.size:9.4f} "
            )

    def notify_trade(self, trade):
        """Provides notification of closed trades."""
        if trade.isclosed:
            self.log(
                "{} Closed: PnL Gross {}, Net {},".format(
                    trade.data._name,
                    round(trade.pnl, 2),
                    round(trade.pnlcomm, 1),
                )
            )

    def next(self):
        if self.position.size == 0:
            if self.crossover > 0:
                amount_to_invest = self.params.order_percentage * self.broker.cash
                self.size = math.floor(amount_to_invest / self.data.close)

                self.log(
                    "Buy {} shares of {} at {}".format(
                        self.size,
                        self.params.ticker,
                        self.data.close[0],
                    )
                )
                self.buy(size=self.size)

        if self.position.size > 0:
            if self.crossover < 0:
                self.log(
                    "Sell {} shares of {} at {}".format(
                        self.size, self.params.ticker, self.data.close[0],
                    )
                )
                self.sell(size=self.size)


if __name__ == "__main__":
    run_test_macd()

With a print out as follows:

starting protfolio value: 1000000.00
position size: 0
2019-10-28, Buy 15105 shares of stock at 65.54
2019-10-29, TSLA   Order:   1   Type: Buy   Status Accepted     Size: 15105.0000 Price:   65.5400 Position: 15105
2019-10-29, TSLA   Order:   1   Type: Buy   Status Completed    Size: 15105.0000 Price:   65.5400 Position: 15105
2019-10-29, TSLA   BUY   Price:  64.00 Cost: 966720.00 Comm: 0.00 Size: 15105.0000 
final protfolio value: 10527931.90
starting protfolio value: 1000000.00
position size: 0
2020-05-12, Buy 4712 shares of stock at 210.1
2020-05-13, FB     Order:   2   Type: Buy   Status Accepted     Size: 4712.0000 Price:  210.1000 Position: 4712
2020-05-13, FB     Order:   2   Type: Buy   Status Completed    Size: 4712.0000 Price:  210.1000 Position: 4712
2020-05-13, FB     BUY   Price: 209.43 Cost: 986834.16 Comm: 0.00 Size: 4712.0000 
final protfolio value: 1294217.28

Process finished with exit code 0
run-out
  • 3,114
  • 1
  • 9
  • 25
  • I ran into some issues with this code. 1. the actual trade price(write down in file) is different from the output. 2/ the actual trade price is not any of the open low high close adj close. 3. when the order percentage set to 100% often show order status: margin and order canceled even when cash is enough to support the trade. i change the code to 'self.buy(size=self.size,price=self.data.close[0])' 'self.sell(size=self.size,price=self.data.close[0])' still won't solve the problem. – DMG Apr 09 '21 at 15:04
  • You need a buffer here: `self.size = math.floor(amount_to_invest / self.data.close)` If you try to invest the whole amount, when calculating shares, if price goes up next bar, not enough cash to buy. Try this `self.size = math.floor((amount_to_invest * .9) / self.data.close)` Depending on volatility of stock, you can use .98 or higher. Just experiment. – run-out Apr 09 '21 at 15:12
  • ```the actual trade price(write down in file) is different from the output. 2/ the actual trade price is not any of the open low high close adj close.``` You are using `market` order, check your data, this should be transact at next open. – run-out Apr 09 '21 at 15:15
  • 1
    run-out, you have been an amazing help. I am pretty new to this. thank you very much for your kindness. I have added two pictures. showing the buy/sell order price and position problems. – DMG Apr 10 '21 at 00:59
  • run-out. your code runs, but the buying price and selling price is not correct. I attached two new pictures to the post. can you see it? – DMG Apr 10 '21 at 01:53
  • Your image with the margin status makes sense. The next day open price jumps up increasing the cost of the purchase past the available cash, so the purchase is margined out. I cannot reconcile the other image with the price data. You have to print out the prices in your backtrader as a log to terminal so you can better see what's going on with the prices. – run-out Apr 10 '21 at 10:04
  • the picture is updated. two questions. 1. instead of buy/sell next day open price, is there a possibility to buy at current day close price? 2. the cerebro.addwriter(bt.WriterFile...) write into the file. so is the writer function output wrong? – DMG Apr 10 '21 at 15:05
  • No, you cannot buy at close for the bar past, no more than you can in real life. I don't use addwriter but I suspect it's not wrong, since the code base is mature and I haven't heard any problems with this. – run-out Apr 10 '21 at 20:18
  • in real life, you can always buy at the close price at aftermarket. the trading volume is low, but if small positions, still can get the very near the close price. I just need the close price for study. I suspect it has nothing wrong with the addwriter but I don't know where the numbers come from, and I don't know the other way to recode the buy and sell data. I am processing 505 company data, I am kind of looking for a systematic way to do this. – DMG Apr 11 '21 at 02:18