From 645bf4c50bd817ecd5dceca03a1416f92ebaf54a Mon Sep 17 00:00:00 2001 From: pranjal-joshi Date: Sun, 26 Nov 2023 22:47:01 +0530 Subject: [PATCH] 2.17 - Backtest report generation added - Backtest report improved - Backtesting report engine bugfixed --- src/classes/Changelog.py | 5 +++- src/classes/Fetcher.py | 43 ++++++++++++++++++++++++++++++- src/classes/ParallelProcessing.py | 9 ++++++- src/classes/Utility.py | 14 ++++++++++ src/release.md | 2 +- src/screenipy.py | 3 ++- src/streamlit_app.py | 8 +++--- 7 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/classes/Changelog.py b/src/classes/Changelog.py index 6b04226e..f2f9e4bf 100644 --- a/src/classes/Changelog.py +++ b/src/classes/Changelog.py @@ -7,7 +7,7 @@ from classes.ColorText import colorText -VERSION = "2.16" +VERSION = "2.17" changelog = colorText.BOLD + '[ChangeLog]\n' + colorText.END + colorText.BLUE + ''' [1.00 - Beta] @@ -274,4 +274,7 @@ [2.16] 1. Nifty Prediction NaN values handled gracefully with forward filling if data is absent 2. Ticker 0 > Search by Stock name - re-enabled in GUI + +[2.17] +1. Backtest Report column added for backtest screening runs ''' + colorText.END diff --git a/src/classes/Fetcher.py b/src/classes/Fetcher.py index 93642c8d..2a176472 100644 --- a/src/classes/Fetcher.py +++ b/src/classes/Fetcher.py @@ -51,6 +51,27 @@ def _getBacktestDate(self, backtest): return [start, end] except: return [None, None] + + def _getDatesForBacktestReport(self, backtest): + dateDict = {} + try: + today = datetime.date.today() + dateDict['T+1d'] = backtest + datetime.timedelta(days=1) if backtest + datetime.timedelta(days=1) < today else None + dateDict['T+1wk'] = backtest + datetime.timedelta(weeks=1) if backtest + datetime.timedelta(weeks=1) < today else None + dateDict['T+1mo'] = backtest + datetime.timedelta(days=30) if backtest + datetime.timedelta(days=30) < today else None + dateDict['T+6mo'] = backtest + datetime.timedelta(days=180) if backtest + datetime.timedelta(days=180) < today else None + dateDict['T+1y'] = backtest + datetime.timedelta(days=365) if backtest + datetime.timedelta(days=365) < today else None + for key, val in dateDict.copy().items(): + if val is not None: + if val.weekday() == 5: # 5 is Saturday, 6 is Sunday + adjusted_date = val + datetime.timedelta(days=2) + dateDict[key] = adjusted_date + elif val.weekday() == 6: + adjusted_date = val + datetime.timedelta(days=1) + dateDict[key] = adjusted_date + except: + pass + return dateDict def fetchCodes(self, tickerOption,proxyServer=None): listStockCodes = [] @@ -138,6 +159,7 @@ def fetchStockCodes(self, tickerOption, proxyServer=None): # Fetch stock price data from Yahoo finance def fetchStockData(self, stockCode, period, duration, proxyServer, screenResultsCounter, screenCounter, totalSymbols, backtestDate=None, printCounter=False, tickerOption=None): + dateDict = None with SuppressOutput(suppress_stdout=True, suppress_stderr=True): append_exchange = ".NS" if tickerOption == 15: @@ -152,6 +174,25 @@ def fetchStockData(self, stockCode, period, duration, proxyServer, screenResults start=self._getBacktestDate(backtest=backtestDate)[0], end=self._getBacktestDate(backtest=backtestDate)[1] ) + if backtestDate != datetime.date.today(): + dateDict = self._getDatesForBacktestReport(backtest=backtestDate) + backtestData = yf.download( + tickers=stockCode + append_exchange, + interval='1d', + proxy=proxyServer, + progress=False, + timeout=10, + start=backtestDate - datetime.timedelta(days=1), + end=backtestDate + datetime.timedelta(days=370) + ) + for key, value in dateDict.copy().items(): + if value is not None: + try: + dateDict[key] = backtestData.loc[pd.Timestamp(value)]['Close'] + except KeyError: + continue + dateDict['T+52wkH'] = backtestData['High'].max() + dateDict['T+52wkL'] = backtestData['Low'].min() if printCounter: sys.stdout.write("\r\033[K") try: @@ -166,7 +207,7 @@ def fetchStockData(self, stockCode, period, duration, proxyServer, screenResults return None print(colorText.BOLD + colorText.GREEN + "=> Done!" + colorText.END, end='\r', flush=True) - return data + return data, dateDict # Get Daily Nifty 50 Index: def fetchLatestNiftyDaily(self, proxyServer=None): diff --git a/src/classes/ParallelProcessing.py b/src/classes/ParallelProcessing.py index d9d26c39..9ce8d7bd 100644 --- a/src/classes/ParallelProcessing.py +++ b/src/classes/ParallelProcessing.py @@ -80,7 +80,7 @@ def screenStocks(self, tickerOption, executeOption, reversalOption, maLength, da if (self.stockDict.get(stock) is None) or (configManager.cacheEnabled is False) or self.isTradingTime or downloadOnly: try: - data = fetcher.fetchStockData(stock, + data, backtestReport = fetcher.fetchStockData(stock, period, configManager.duration, self.proxyServer, @@ -198,6 +198,13 @@ def screenStocks(self, tickerOption, executeOption, reversalOption, maLength, da with SuppressOutput(suppress_stderr=True, suppress_stdout=True): isLorentzian = screener.validateLorentzian(fullData, screeningDictionary, saveDictionary, lookFor = maLength) + try: + backtestReport = Utility.tools.calculateBacktestReport(data=processedData, backtestDict=backtestReport) + screeningDictionary.update(backtestReport) + saveDictionary.update(backtestReport) + except: + pass + with self.screenResultsCounter.get_lock(): if executeOption == 0: self.screenResultsCounter.value += 1 diff --git a/src/classes/Utility.py b/src/classes/Utility.py index 78cbff7c..bc3e87cf 100644 --- a/src/classes/Utility.py +++ b/src/classes/Utility.py @@ -384,6 +384,20 @@ def isBacktesting(backtestDate): return False except: return False + + def calculateBacktestReport(data, backtestDict:dict): + try: + recent = data.head(1)['Close'].iloc[0] + for key, val in backtestDict.items(): + if val is not None: + try: + backtestDict[key] = str(round((backtestDict[key]-recent)/recent*100,1)) + "%" + except TypeError: + backtestDict[key] = None + continue + except: + pass + return backtestDict def isDocker(): if 'SCREENIPY_DOCKER' in os.environ: diff --git a/src/release.md b/src/release.md index 7280a6da..b3ea26f3 100644 --- a/src/release.md +++ b/src/release.md @@ -7,7 +7,7 @@ Screeni-py is now on **YouTube** for additional help! - Thank You for your suppo ⚠️ **Executable files (.exe, .bin and .run) are now DEPRECATED! Please Switch to Docker** -1. **Backtesting** Added for Screening Patterns to Develope and Test Strategies! +1. **Backtesting Reports** Added for Screening Patterns to Develope and Test Strategies! 2. **Position Size Calculator** tab added for Better and Quick Risk Management! 3. **Lorentzian Classification** (by @jdehorty) added for enhanced accuracy for your trades - - Try `Option > 6 > 7` 🤯 4. **Artificial Intelligence v3 for Nifty 50 Prediction** - Predict Next day Gap-up/down using Nifty, Gold and Crude prices! - Try `Select Index for Screening > N` diff --git a/src/screenipy.py b/src/screenipy.py index 7957f913..ecbc28c2 100644 --- a/src/screenipy.py +++ b/src/screenipy.py @@ -478,7 +478,8 @@ def main(testing=False, testBuild=False, downloadOnly=False, execute_inputs:list matchedSaveResults = pd.concat([matchedSaveResults, saveResults[saveResults['Stock'].str.contains(stk)]], ignore_index=True) screenResults, saveResults = matchedScreenResults, matchedSaveResults - + screenResults.dropna(axis=1, how='all', inplace=True) + saveResults.dropna(axis=1, how='all', inplace=True) screenResults.sort_values(by=['Stock'], ascending=True, inplace=True) saveResults.sort_values(by=['Stock'], ascending=True, inplace=True) screenResults.set_index('Stock', inplace=True) diff --git a/src/streamlit_app.py b/src/streamlit_app.py index 9425f0ea..fc52e52c 100644 --- a/src/streamlit_app.py +++ b/src/streamlit_app.py @@ -44,7 +44,7 @@ def check_updates(): def show_df_as_result_table(): try: - df = pd.read_pickle('last_screened_unformatted_results.pkl') + df:pd.DataFrame = pd.read_pickle('last_screened_unformatted_results.pkl') ac, bc = st.columns([6,1]) ac.markdown(f'#### 🔍 Found {len(df)} Results') bc.download_button( @@ -100,7 +100,7 @@ def dummy_call(): os.environ['SCREENIPY_REQ_ERROR'] = "TRUE" if Utility.tools.isBacktesting(backtestDate=backtestDate): - st.write(f'Running in :red[Backtesting Mode] for {str(backtestDate)} (Y-M-D)') + st.write(f'Running in :red[**Backtesting Mode**] for *T = {str(backtestDate)}* (Y-M-D) : [Backtesting data is subjected to availability as per the API limits]') t = Thread(target=dummy_call) t.start() @@ -175,8 +175,8 @@ def get_extra_inputs(tickerOption, executeOption, c_index=None, c_criteria=None, if not tickerOption.isnumeric(): execute_inputs = [tickerOption, 0, 'N'] elif int(tickerOption) == 0 or tickerOption is None: - stock_codes = c_index.text_input('Enter Stock Code(s)', placeholder='SBIN, INFY, ITC') - execute_inputs = [tickerOption, executeOption, stock_codes, 'N'] + stock_codes:str = c_index.text_input('Enter Stock Code(s)', placeholder='SBIN, INFY, ITC') + execute_inputs = [tickerOption, executeOption, stock_codes.upper(), 'N'] elif int(executeOption) >= 0 and int(executeOption) < 4: execute_inputs = [tickerOption, executeOption, 'N'] elif int(executeOption) == 4: