Skip to content

Commit

Permalink
2.17 - Backtest report generation added
Browse files Browse the repository at this point in the history
- Backtest report improved
- Backtesting report engine bugfixed
  • Loading branch information
pranjal-joshi committed Nov 27, 2023
1 parent 59feb17 commit 645bf4c
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 9 deletions.
5 changes: 4 additions & 1 deletion src/classes/Changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
43 changes: 42 additions & 1 deletion src/classes/Fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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):
Expand Down
9 changes: 8 additions & 1 deletion src/classes/ParallelProcessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/classes/Utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
3 changes: 2 additions & 1 deletion src/screenipy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions src/streamlit_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 645bf4c

Please sign in to comment.