A Different Approach to Passive Investing - [Help]
-
Good morning all,
I am new to the Back Trader community and I am attempting to create some simple back tests but I'm struggling. Essentially my idea is a slightly unique take on Dollar-Cost Averaging. I want to first evaluate the effectiveness of simply Dollar-Cost Averaging (Which is done neatly in the article "Buy and Buy More" on the Back Trader site). This means to essentially invest a fixed amount at regular time intervals (For example $500 at the start of each month as I'm sure you're all aware of).
However, I wish to alter the amount invested depending on recent trends in the market. For example if the S&P500 (The ETF I am backtesting on), has had a correction then I would like to invest more than usual. However if it has risen steeply in the past month invest a little less.
I am encountering issues as I seem to be able to only buy whole units of the ETF and I really need to be able to test fractional amounts which I have found an article relating to but can't implement it with the "Buy and Buy More" article.
The strategy would look something like this:
S&P500 5% Monthly Increase = Invest 0.8 * Standard Monthly Amount
S&P500 1% Monthly Increase = Invest 1 * Standard Monthly Amount
S&P500 3% Monthly Decrease = Invest 1.2 * Standard Monthly Amount
S&P500 5% Monthly Decrease = Invest 1.4 * Standard Monthly AmountIs it possible to change the monthly investable amount such as described here and buy fractional units of an ETF based on recent price fluctuations. I am essentially trying to return slightly more than the market by adding a bit of optimisation to Dollar-Cost Averaging.
If you have any ideas on my strategy, tips or tricks or just general advice or criticism I would love to hear it. Thanks for taking the time to read my post and I eagerly await any comments made :)
Kind regards,
James -
@james-scarr
My code so far if anyone wants to play with it:import datetime
import backtrader as bt
from pandas_datareader import data as pdrGet stock data from yahoo
def get_data(stock, start, end):
stockData = pdr.get_data_yahoo(stock, start, end)
return stockDataUse the S&P500 Symbol ^GSPC on Yahoo Finance
stock = '^GSPC'
Choose stock data to retrieve and set a start and end date
endDate = datetime.datetime(2021,1,1)
startDate = datetime.datetime(2020,1,1)
stockData = get_data(stock, startDate, endDate)Actual start date is the start of the following day
actualStart = stockData.index[0]
Put data in a format that BackTrader understands
data = bt.feeds.PandasData(dataname=stockData)
######################
class CommInfoFractional(bt.CommissionInfo):
def getsize(self, price, cash):
'''Returns fractional size for cash operation @price'''
return self.p.leverage * (cash / price)######################
class FixedCommisionScheme(bt.CommInfoBase):
paras = (
('commission', 10),
('stocklike', True),
('commtype', bt.CommInfoBase.COMM_FIXED)
)def _getcommission(self, size, price, pseudoexec): return self.p.commission
#####################
class PoundCostAveraging(bt.Strategy):
params = dict(
monthly_cash=500,
monthly_range=[1]
)def __init__(self): # Keep track of variables self.order = None self.totalcost = 0 self.cost_wo_bro = 0 self.units = 0 self.times = 0 def log(self, txt, dt=None): # Print the trades as they occur dt = dt or self.datas[0].datetime.date(0) print(dt.isoformat() + ', ' + txt) def start(self): self.broker.set_fundmode(fundmode=True, fundstartval=100.0) self.cash_start = self.broker.get_cash() self.val_start = 100.0 # ADD A TIMER self.add_timer( when=bt.timer.SESSION_START, monthdays=[i for i in self.p.monthly_range], monthcarry=True # timername='buytimer', ) def notify_timer(self, timer, when, *args): self.broker.add_cash(self.p.monthly_cash) target_value = self.broker.get_value() + self.p.monthly_cash - 10 self.order_target_value(target=target_value) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: return if order.status in [order.Completed]: if order.isbuy(): self.log( 'BUY EXECUTED, Price %.2f, Cost %.2f, Comm %.2f, Size %.0f' % (order.executed.price, order.executed.value, order.executed.comm, order.executed.size) ) self.units += order.executed.size self.totalcost += order.executed.value + order.executed.comm self.cost_wo_bro += order.executed.value self.times += 1 elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') print(order.status, [order.Canceled, order.Margin, order.Rejected]) self.order = None def stop(self): # calculate actual returns self.roi = (self.broker.get_value() / self.cash_start) - 1 self.froi = (self.broker.get_fundvalue() - self.val_start) value = self.datas[0].close * self.units + self.broker.get_cash() print('-'*50) print('PoundCostAveraging') print('For year ending in ' + str(endDate)) print('Time in Market: {:.1f} years'.format((endDate - actualStart).days/365)) print('#Times: {:.0f}'.format(self.times)) print('Value: ${:,.2f}'.format(value)) print('Cost: ${:,.2f}'.format(self.totalcost)) print('Gross Return: ${:,.2f}'.format(value - self.totalcost)) print('Gross %: {:.2f}%'.format((value/self.totalcost - 1) * 100)) print('ROI: {:.2f}%'.format(100.0 * self.roi)) print('Fund Value: {:.2f}%'.format(self.froi)) print('Annualised: {:.2f}%'.format(100*((1+self.froi/100)**(365/(endDate - actualStart).days) - 1))) print('-'*50)
########################
def run(data):
# Pound Cost Averaging
cerebro = bt.Cerebro()
cerebro.adddata(data)
cerebro.addstrategy(PoundCostAveraging)# Broker Information broker_args = dict(coc=True) cerebro.broker = bt.brokers.BackBroker(**broker_args) cerebro.broker.addcommissioninfo(CommInfoFractional()) #comminfo = FixedCommisionScheme() #cerebro.broker.addcommissioninfo(comminfo) cerebro.broker.set_cash(500) cerebro.run() cerebro.plot(iplot=False, style='candlestick')
if name == 'main':
run(data) -
@james-scarr
I would like to look at this but could you format the code by putting it between triple quotes as per the instructions at the top of the page? Thanks.For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/
-
@run-out sorry about that!
import datetime import backtrader as bt from pandas_datareader import data as pdr # Get stock data from yahoo stock = '^GSPC' def get_data(stock, start, end): stockData = pdr.get_data_yahoo(stock, start, end) return stockData endDate = datetime.datetime(2021,1,1) startDate = datetime.datetime(2020,1,1) stockData = get_data(stock, startDate, endDate) # Actual start date is the start of the following day actualStart = stockData.index[0] # Put data in a format that BackTrader understands data = bt.feeds.PandasData(dataname=stockData) ###################### class CommInfoFractional(bt.CommissionInfo): def getsize(self, price, cash): '''Returns fractional size for cash operation @price''' return self.p.leverage * (cash / price) ###################### class FixedCommisionScheme(bt.CommInfoBase): paras = ( ('commission', 10), ('stocklike', True), ('commtype', bt.CommInfoBase.COMM_FIXED) ) def _getcommission(self, size, price, pseudoexec): return self.p.commission ##################### class PoundCostAveraging(bt.Strategy): params = dict( monthly_cash=500, monthly_range=[1] ) def __init__(self): # Keep track of variables self.order = None self.totalcost = 0 self.cost_wo_bro = 0 self.units = 0 self.times = 0 self.prices = [] def log(self, txt, dt=None): # Print the trades as they occur dt = dt or self.datas[0].datetime.date(0) print(dt.isoformat() + ', ' + txt) def start(self): self.broker.set_fundmode(fundmode=True, fundstartval=100.0) self.cash_start = self.broker.get_cash() self.val_start = 100.0 # ADD A TIMER self.add_timer( when=bt.timer.SESSION_START, monthdays=[i for i in self.p.monthly_range], monthcarry=True # timername='buytimer', ) def notify_timer(self, timer, when, *args): if len(self.prices) > 2: if self.prices[-1] > self.prices[-2]: highFigure = self.p.monthly_cash * 1.2 self.broker.add_cash(highFigure) target_value = (self.broker.get_value() + highFigure) - 10 self.order_target_value(target=target_value) else: lowFigure = self.p.monthly_cash * 0.8 self.broker.add_cash(lowFigure) target_value = (self.broker.get_value() + lowFigure) - 10 self.order_target_value(target=target_value) else: self.broker.add_cash(self.p.monthly_cash) target_value = (self.broker.get_value() + self.p.monthly_cash) - 10 self.order_target_value(target=target_value) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: return if order.status in [order.Completed]: if order.isbuy(): self.log( 'BUY EXECUTED, Price %.2f, Cost %.2f, Comm %.2f, Size %.0f' % (order.executed.price, order.executed.value, order.executed.comm, order.executed.size) ) self.units += order.executed.size self.totalcost += order.executed.value + order.executed.comm self.cost_wo_bro += order.executed.value self.times += 1 self.prices.append(order.executed.price) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') print(order.status, [order.Canceled, order.Margin, order.Rejected]) self.order = None def stop(self): # calculate actual returns self.roi = (self.broker.get_value() / self.cash_start) - 1 self.froi = (self.broker.get_fundvalue() - self.val_start) value = self.datas[0].close * self.units + self.broker.get_cash() print('-'*50) print('PoundCostAveraging') print('For year ending in ' + str(endDate)) print('Time in Market: {:.1f} years'.format((endDate - actualStart).days/365)) print('#Times: {:.0f}'.format(self.times)) print('Value: ${:,.2f}'.format(value)) print('Cost: ${:,.2f}'.format(self.totalcost)) print('Gross Return: ${:,.2f}'.format(value - self.totalcost)) print('Gross %: {:.2f}%'.format((value/self.totalcost - 1) * 100)) print('ROI: {:.2f}%'.format(100.0 * self.roi)) print('Fund Value: {:.2f}%'.format(self.froi)) print('Annualised: {:.2f}%'.format(100*((1+self.froi/100)**(365/(endDate - actualStart).days) - 1))) print('-'*50) ######################## def run(data): # Pound Cost Averaging cerebro = bt.Cerebro() cerebro.adddata(data) cerebro.addstrategy(PoundCostAveraging) # Broker Information broker_args = dict(coc=True) cerebro.broker = bt.brokers.BackBroker(**broker_args) #Choose between comission schemes first is for fractional amounts second charges you $10 per buy cerebro.broker.addcommissioninfo(CommInfoFractional()) #cerebro.broker.addcommissioninfo(FixedCommisionScheme()) cerebro.broker.set_cash(500) cerebro.run() cerebro.plot(iplot=False, style='candlestick') if __name__ == '__main__': run(data)
-
@james-scarr You code seems to be working to me. The only minor error I saw was that you are printing to console the units formatting to
0
decimals, so you only see zeros since the stock price is higher than $500. Change like this:self.log( 'BUY EXECUTED, Price %.2f, Cost %.2f, Comm %.2f, Size %.4f' % (order.executed.price, order.executed.value, order.executed.comm, order.executed.size) )
-
@run-out
Thank you very much I'll make that adjustment!I don't suppose you know how to alter the indicators in Cerebro.plot() so that I could perhaps change the colour of the buy signal on the graph depending on the value invested (e.g. blue for £800, green for £1000, etc)
-
@james-scarr Sorry I don't use the built in plotting.
-
@run-out No worries.
Just wanted to let you know that if you did end up using my code it has a major flaw in it.
Under the function notify_timer() I compare the two most recent orders with self.prices[-1] > self.prices[-2]. This logic is completely wrong as I am comparing orders that have already happened and then using that data to decide on the next trade.For example on March 1st I am comparing whether February was lower than January and then deciding whether to buy more in March. Whereas I should be checking whether March is lower than February.
Sorry! -
@run-out
This can be fixed by changing to:def notify_timer(self, timer, when, *args): # If we bought last month then compare last month to this month # determine how much more or less to invest depending on this difference if len(self.prices) > 1: percentageChange = round(((self.data[0]/self.prices[-1])-1)*100,2) #print(f"The percentage change was {percentageChange}%") print(f'These are the prices: {percentageChange}') if percentageChange < -1: lowFigure = self.p.monthly_cash * 1.4 self.broker.add_cash(lowFigure) target_value = (self.broker.get_value() + lowFigure) - 10 self.order_target_value(target=target_value) else: highFigure = self.p.monthly_cash * 1 self.broker.add_cash(highFigure) target_value = (self.broker.get_value() + highFigure) - 10 self.order_target_value(target=target_value) else: self.broker.add_cash(self.p.monthly_cash) target_value = (self.broker.get_value() + self.p.monthly_cash) - 10 self.order_target_value(target=target_value)
Unfortunately I seem to be finding that this strategy does not perform much better at all compared to dollar-cost averaging. :(
-
@run-out
Would you be able to remove this thread from Backtrader as well as the other one I posted? I am trying to delete them but I don't think I can and I would rather my code wasn't publicly available :)
Kind regards,
James