diff --git a/compare.py b/compare.py index fffd824..89484f9 100644 --- a/compare.py +++ b/compare.py @@ -18,13 +18,24 @@ def get_data(stocks, start, end): cache_name="cache", backend="sqlite", expire_after=expire_after ) session.headers = DEFAULT_HEADERS - start = datetime.datetime.fromisoformat("1900-01-01") - end = datetime.datetime.now() - stockData = pdr.get_data_yahoo(stocks, start, end, session=session) - return stockData + stockData = pdr.get_data_yahoo( + stocks, + datetime.datetime.fromisoformat("1900-01-01"), + datetime.datetime.now(), + session=session, + ) + return stockData.loc[start:end] + + +def human_readable_size(size, decimal_places=3): + for unit in ["$", "K$", "M$", "G$", "T$", "P$"]: + if size < 1000.0 or unit == "P$": + return f"{size:.{decimal_places}f} {unit}" + break + size /= 1000.0 -def run(strategy, data, fund_mode=False): +def prepare_simulation(strategy, params, data, fund_mode=False): if fund_mode: cerebro = bt.Cerebro() cerebro.addobserver(bt.observers.FundShares) @@ -34,7 +45,7 @@ def run(strategy, data, fund_mode=False): cerebro.addobserver(bt.observers.BuySell) cerebro.adddata(data) - cerebro.addstrategy(strategy) + cerebro.addstrategy(strategy, params) # Broker Information broker_args = dict(coc=True) @@ -45,68 +56,100 @@ def run(strategy, data, fund_mode=False): if fund_mode: cerebro.broker.set_fundmode(True) - cerebro.broker.set_cash(st.month_sum) cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown") cerebro.addanalyzer(bt.analyzers.VWR, _name="returns") cerebro.addanalyzer( bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.NoTimeFrame, _name="tr" ) - thestrats = cerebro.run() - thestrat = thestrats[0] - # cerebro.plot(iplot=False, style="candlestick") - return thestrat + return cerebro -if __name__ == "__main__": +def simulate(stockData, monthly_params): + pd.options.display.float_format = "{:,.2f}".format df = pd.DataFrame( columns=[ - "froi", + "froi%", "cost", "total_value", - "times", - "units", + "#deals", + "#units", "comms", + "monthly", "annual%", - "max_dd_len", - "max_dd", + "max_dd_days", + "max_dd%", "max_md", - "twr", + "vwr", "tr", ] ) + actualStart: datetime.datetime = stockData.index[0] + actualEnd: datetime.datetime = stockData.index[-1] + data = bt.feeds.PandasData(dataname=stockData) + + for i, strategy in enumerate((st.DCA, st.QDCA, st.VA, st.QVA)): + cerebro = prepare_simulation(strategy, monthly_params, data) + cerebro.broker.set_cash(monthly_params["sum"]) + therun = cerebro.run()[0] + # cerebro.plot(iplot=False, style="candlestick") + dd = therun.analyzers.drawdown + ret = therun.analyzers.returns + tr = therun.analyzers.tr + # print(next(reversed(tr.get_analysis()))) + params = therun.calc_params() + # omg IM so sorry for this, ironically this is here to get human readable size + for i in 1, 2, 5, 6: + params[i] = human_readable_size(params[i]) + annual = 100 * ( + (1 + params[0] / 100) ** (365 / (actualEnd - actualStart).days) - 1 + ) + df.loc[strategy.__name__] = params + [ + annual, + dd.get_analysis().max.len, + dd.get_analysis().max.drawdown, + human_readable_size(dd.get_analysis().max.moneydown), + ret.get_analysis()["vwr"], + list(tr.get_analysis().items())[0][1], + ] + print( + "Investing monthly, increasing {:.2f}%, starting from ${}, from {} to {}, {:.1f} years".format( + (monthly_params["coef"] * 100) - 100, + monthly_params["sum"], + actualStart.date(), + actualEnd.date(), + (actualEnd - actualStart).days / 365, + ) + ) + # print(df[["annual%", "froi%", "cost", "total_value", "max_dd%", "max_md"]]) + print(df) + + +if __name__ == "__main__": stockList = ["^GSPC"] + monthly_params = dict(sum=1000, coef=1.001) # startDate = datetime.datetime.fromisoformat("2000-01-01") # endDate = datetime.datetime.fromisoformat("2022-01-01") print(stockList[0]) - for period_years in (20, 50): - endDate = datetime.datetime.now() - startDate = endDate - datetime.timedelta(days=period_years * 365) - stockData = get_data(stockList[0], startDate, endDate) - actualStart: datetime.datetime = stockData.index[0] - data = bt.feeds.PandasData(dataname=stockData) - - for i, strategy in enumerate( - (st.SmaCross, st.DCA, st.QDCA, st.VA, st.QVA, st.SmaVA) - ): - therun = run(strategy, data) - dd = therun.analyzers.drawdown - ret = therun.analyzers.returns - tr = therun.analyzers.tr - # print(next(reversed(tr.get_analysis()))) - params = therun.calc_params() - annual = 100 * ( - (1 + params[0] / 100) ** (365 / (endDate - actualStart).days) - 1 - ) - df.loc[strategy.__name__] = therun.calc_params() + ( - annual, - dd.get_analysis().max.len, - dd.get_analysis().max.drawdown, - dd.get_analysis().max.moneydown, - ret.get_analysis()["vwr"], - list(tr.get_analysis().items())[0][1], - ) - print("Starting from:", actualStart) - print("Time in Market: {:.1f} years".format((endDate - actualStart).days / 365)) - # print(df[["annual%", "froi", "cost", "total_value", "max_dd", "max_md"]]) - print(df) + for period_years in (1, 2, 5, 10, 20, 50, 100): + end_date = datetime.datetime.now() + start_date = end_date - datetime.timedelta(days=period_years * 365) + stockData = get_data(stockList[0], start_date, end_date) + simulate(stockData, monthly_params) + """ + period_years = 10 + stockData = get_data( + stockList[0], + datetime.datetime.fromisoformat("1900-01-01"), + datetime.datetime.now(), + ) + start_date = stockData.index[0] + end_date = start_date + datetime.timedelta(days=period_years * 365) + while start_date < datetime.datetime.today(): + stockData = get_data(stockList[0], start_date, end_date) + simulate(stockData) + start_date, end_date = ( + end_date, + end_date + datetime.timedelta(days=period_years * 365), + ) + """ diff --git a/strategies.py b/strategies.py index b811f82..a1ad3a3 100644 --- a/strategies.py +++ b/strategies.py @@ -7,8 +7,6 @@ from pandas_datareader import data as pdr import backtrader as bt reserve = 5 # usd for comms etc cause I cant math -month_sum = 5000 # usd - """ class LSI(bt.Strategy): @@ -47,7 +45,8 @@ class PercentageCommisionScheme(bt.CommInfoBase): class Investing(bt.Strategy): - def __init__(self): + def __init__(self, params={"sum": 500, "coef": 1}): + self.monthly_params = params self.order = None self.cost = 0 # no comms self.comms = 0 @@ -105,21 +104,26 @@ class Investing(bt.Strategy): self.order = None + @property + def monthly_cash(self): + return self.monthly_params["sum"] * self.monthly_params["coef"] ** (self.months) + def calc_params(self): # calculate actual returns self.froi = self.broker.get_fundvalue() - self.val_start - return ( + return [ self.froi, self.cost, self.broker.get_value(), self.times, self.units, self.comms, - ) + self.monthly_cash, + ] def notify_timer(self, timer, when, *args): self.months += 1 - self.broker.add_cash(month_sum * 1.005 ** (self.months)) + self.broker.add_cash(self.monthly_cash) class FormulaInvesting(Investing): @@ -132,7 +136,7 @@ class VA(FormulaInvesting): def formula(self): target_value = ( min( - self.months * month_sum * 1.005 ** (self.months), + self.months * self.monthly_cash, self.broker.get_value(), ) - reserve @@ -159,14 +163,13 @@ class QDCA(DCA): class SmaCross(Investing): - # list of parameters which are configurable for the strategy params = dict( pfast=50, # period for the fast moving average pslow=200, # period for the slow moving average ) - - def __init__(self): - super().__init__() + # list of parameters which are configurable for the strategy + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) sma1 = bt.ind.SMA(period=self.p.pfast) # fast moving average sma2 = bt.ind.SMA(period=self.p.pslow) # slow moving average self.crossover = bt.ind.CrossOver(sma1, sma2) # crossover signal