Skip to content

Commit

Permalink
v1.34 - Narrow Range Reversal and Other Improvements
Browse files Browse the repository at this point in the history
[Screenipy Test] New Features Added - Test Passed - Merge OK
closes #50
  • Loading branch information
pranjal-joshi authored Feb 1, 2022
2 parents 8944f21 + a895642 commit c75db42
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 32 deletions.
7 changes: 6 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 = "1.33"
VERSION = "1.34"

changelog = colorText.BOLD + '[ChangeLog]\n' + colorText.END + colorText.BLUE + '''
[1.00 - Beta]
Expand Down Expand Up @@ -159,5 +159,10 @@
1. Alternate Data source added.
2. Workflow added to create cache data on cloud.
[1.34]
1. New Reversal - Narrow Range : Try Option 6 > 6
2. Cache loading fixes for Pre-Market timings. Refer PR #103
3. Progressbar added for Alternate Source Cache Download.
--- END ---
''' + colorText.END
11 changes: 10 additions & 1 deletion src/classes/ParallelProcessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,15 @@ def screenStocks(self, executeOption, reversalOption, maLength, daysForLowestVol
isVSA = False
if not (executeOption == 7 and respChartPattern < 3):
isVSA = screener.validateVolumeSpreadAnalysis(processedData, screeningDictionary, saveDictionary)
if maLength is not None and executeOption == 6:
if maLength is not None and executeOption == 6 and reversalOption == 4:
isMaSupport = screener.findReversalMA(fullData, screeningDictionary, saveDictionary, maLength)

with SuppressOutput(suppress_stderr=True, suppress_stdout=True):
if maLength is not None and executeOption == 6 and reversalOption == 6:
isNR = screener.validateNarrowRange(processedData, screeningDictionary, saveDictionary, nr=maLength)
else:
isNR = screener.validateNarrowRange(processedData, screeningDictionary, saveDictionary)

isVCP = False
if respChartPattern == 4:
with SuppressOutput(suppress_stderr=True, suppress_stdout=True):
Expand Down Expand Up @@ -203,6 +209,9 @@ def screenStocks(self, executeOption, reversalOption, maLength, daysForLowestVol
elif reversalOption == 5 and isVSA and saveDictionary['Pattern'] in CandlePatterns.reversalPatternsBullish:
self.screenResultsCounter.value += 1
return screeningDictionary, saveDictionary
elif reversalOption == 6 and isNR:
self.screenResultsCounter.value += 1
return screeningDictionary, saveDictionary
if executeOption == 7 and isLtpValid:
if respChartPattern < 3 and isInsideBar:
self.screenResultsCounter.value += 1
Expand Down
17 changes: 17 additions & 0 deletions src/classes/Screener.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,23 @@ def validateNewlyListed(self, data, daysToLookback):
return True
return False

# Find NRx range for Reversal
def validateNarrowRange(self, data, screenDict, saveDict, nr=4):
rangeData = data.head(nr+1)[1:]
now_candle = data.head(1)
rangeData['Range'] = abs(rangeData['Close'] - rangeData['Open'])
recent = rangeData.head(1)
if recent['Range'][0] == rangeData.describe()['Range']['min']:
if self.getCandleType(recent) and now_candle['Close'][0] >= recent['Close'][0]:
screenDict['Pattern'] = colorText.BOLD + colorText.GREEN + f'Buy-NR{nr}' + colorText.END
saveDict['Pattern'] = f'Buy-NR{nr}'
return True
elif not self.getCandleType(recent) and now_candle['Close'][0] <= recent['Close'][0]:
screenDict['Pattern'] = colorText.BOLD + colorText.FAIL + f'Sell-NR{nr}' + colorText.END
saveDict['Pattern'] = f'Sell-NR{nr}'
return True
return False

# Validate VPC
def validateVCP(self, data, screenDict, saveDict, stockName=None, window=3, percentageFromTop=3):
try:
Expand Down
83 changes: 63 additions & 20 deletions src/classes/Utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import pickle
import requests
import pandas as pd
from alive_progress import alive_bar
from tabulate import tabulate
from classes.ColorText import colorText
from classes.Changelog import VERSION, changelog
Expand Down Expand Up @@ -95,13 +96,18 @@ def isTradingTime():
return ((openTime <= curr <= closeTime) and (0 <= curr.weekday() <= 4))

def saveStockData(stockDict, configManager, loadCount):
today_date = datetime.date.today().strftime("%d%m%y")
cache_file = "stock_data_" + str(today_date) + ".pkl"
curr = datetime.datetime.now(pytz.timezone('Asia/Kolkata'))
openTime = curr.replace(hour=9, minute=15)
cache_date = datetime.date.today() # for monday to friday
weekday = datetime.date.today().weekday()
if weekday == 5 or weekday == 6:
last_friday = datetime.datetime.today() - datetime.timedelta(days=weekday - 4)
last_friday = last_friday.strftime("%d%m%y")
cache_file = "stock_data_" + str(last_friday) + ".pkl"
if curr < openTime: # for monday to friday before 9:15
cache_date = datetime.datetime.today() - datetime.timedelta(1)
if weekday == 0 and curr < openTime: # for monday before 9:15
cache_date = datetime.datetime.today() - datetime.timedelta(3)
if weekday == 5 or weekday == 6: # for saturday and sunday
cache_date = datetime.datetime.today() - datetime.timedelta(days=weekday - 4)
cache_date = cache_date.strftime("%d%m%y")
cache_file = "stock_data_" + str(cache_date) + ".pkl"
configManager.deleteStockData(excludeFile=cache_file)

if not os.path.exists(cache_file) or len(stockDict) > (loadCount+1):
Expand All @@ -118,13 +124,18 @@ def saveStockData(stockDict, configManager, loadCount):
"=> Already Cached." + colorText.END)

def loadStockData(stockDict, configManager):
today_date = datetime.date.today().strftime("%d%m%y")
cache_file = "stock_data_" + str(today_date) + ".pkl"
curr = datetime.datetime.now(pytz.timezone('Asia/Kolkata'))
openTime = curr.replace(hour=9, minute=15)
last_cached_date = datetime.date.today() # for monday to friday after 3:30
weekday = datetime.date.today().weekday()
if weekday == 5 or weekday == 6:
last_friday = datetime.datetime.today() - datetime.timedelta(days=weekday - 4)
last_friday = last_friday.strftime("%d%m%y")
cache_file = "stock_data_" + str(last_friday) + ".pkl"
if curr < openTime: # for monday to friday before 9:15
last_cached_date = datetime.datetime.today() - datetime.timedelta(1)
if weekday == 5 or weekday == 6: # for saturday and sunday
last_cached_date = datetime.datetime.today() - datetime.timedelta(days=weekday - 4)
if weekday == 0 and curr < openTime: # for monday before 9:15
last_cached_date = datetime.datetime.today() - datetime.timedelta(3)
last_cached_date = last_cached_date.strftime("%d%m%y")
cache_file = "stock_data_" + str(last_cached_date) + ".pkl"
if os.path.exists(cache_file):
with open(cache_file, 'rb') as f:
try:
Expand All @@ -143,17 +154,31 @@ def loadStockData(stockDict, configManager):
cache_url = "https://raw.github.com/pranjal-joshi/Screeni-py/actions-data-download/actions-data-download/" + cache_file
resp = requests.get(cache_url, stream=True)
if resp.status_code == 200:
print(colorText.BOLD + colorText.FAIL +"[+] After-Market Stock Data is not cached.." + colorText.END)
print(colorText.BOLD + colorText.GREEN +"[+] Downloading cache from Screenipy server for faster processing, This may take a while.." + colorText.END)
print(colorText.BOLD + colorText.FAIL +
"[+] After-Market Stock Data is not cached.." + colorText.END)
print(colorText.BOLD + colorText.GREEN +
"[+] Downloading cache from Screenipy server for faster processing, Please Wait.." + colorText.END)
try:
chunksize = 1024*1024*1
filesize = int(int(resp.headers.get('content-length'))/chunksize)
bar, spinner = tools.getProgressbarStyle()
f = open(cache_file, 'wb')
f.write(resp.content)
dl = 0
with alive_bar(filesize, bar=bar, spinner=spinner, manual=True) as progressbar:
for data in resp.iter_content(chunk_size=chunksize):
dl += 1
f.write(data)
progressbar(dl/filesize)
if dl >= filesize:
progressbar(1.0)
f.close()
except Exception as e:
print("[!] Download Error - " + str(e))
print("")
tools.loadStockData(stockDict, configManager)
else:
print(colorText.BOLD + colorText.FAIL +"[+] Cache unavailable on Screenipy server, Continuing.." + colorText.END)
print(colorText.BOLD + colorText.FAIL +
"[+] Cache unavailable on Screenipy server, Continuing.." + colorText.END)

# Save screened results to excel
def promptSaveResults(df):
Expand Down Expand Up @@ -189,16 +214,26 @@ def promptReversalScreening():
3 > Screen for Momentum Gainers (Rising Bullish Momentum)
4 > Screen for Reversal at Moving Average (Bullish Reversal)
5 > Screen for Volume Spread Analysis (Bullish VSA Reversal)
6 > Screen for Narrow Range (NRx) Reversal
0 > Cancel
[+] Select option: """ + colorText.END))
if resp >= 0 and resp <= 5:
if resp >= 0 and resp <= 6:
if resp == 4:
try:
maLength = int(input(colorText.BOLD + colorText.WARN +
'\n[+] Enter MA Length (E.g. 50 or 200): ' + colorText.END))
return resp, maLength
except ValueError:
print(colorText.BOLD + colorText.FAIL + '\n[!] Invalid Input! MA Lenght should be single integer value!\n' + colorText.END)
print(colorText.BOLD + colorText.FAIL +
'\n[!] Invalid Input! MA Lenght should be single integer value!\n' + colorText.END)
raise ValueError
elif resp == 6:
try:
maLength = int(input(colorText.BOLD + colorText.WARN +
'\n[+] Enter NR timeframe [Integer Number] (E.g. 4, 7, etc.): ' + colorText.END))
return resp, maLength
except ValueError:
print(colorText.BOLD + colorText.FAIL + '\n[!] Invalid Input! NR timeframe should be single integer value!\n' + colorText.END)
raise ValueError
return resp, None
raise ValueError
Expand All @@ -221,12 +256,20 @@ def promptChartPatterns():
return (resp, candles)
if resp == 3:
percent = float(input(colorText.BOLD + colorText.WARN +
"\n[+] Enter Percentage within which all MA/EMAs should be (Ideal: 1-2%)? : " + colorText.END))
"\n[+] Enter Percentage within which all MA/EMAs should be (Ideal: 1-2%)? : " + colorText.END))
return (resp, percent/100.0)
if resp >= 0 and resp <= 4:
return resp, 0
raise ValueError
except ValueError:
input(colorText.BOLD + colorText.FAIL +
"\n[+] Invalid Option Selected. Press Any Key to Continue..." + colorText.END)
return (None, None)
return (None, None)

def getProgressbarStyle():
bar = 'smooth'
spinner = 'waves'
if 'Windows' in platform.platform():
bar = 'classic2'
spinner = 'dots_recur'
return bar, spinner
11 changes: 6 additions & 5 deletions src/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@

Celebrating more than 1,000 Downloads - Thank You for your support <3

1. Alternate Data Source added over Cloud for faster After-Market Analysis!
2. Major BugFixes for **Pattern Detection** - IPO Base Breakout.
1. New Reversal Dection added - **Narrow Range** - Try `Option 6 > 6`
2. Alternate Data Source for faster After-Market Analysis - Optimizations and Cosmetic Updates!
3. **Experimental** Feature - **VCP** Detection - Try `Option > 7 > 4`
4. **New Tickers Group** - Screen only for **Newly Listed IPOs** (Last 1 Yr)
5. **Volume Spread Analysis** : BugFixes for catching Bullish Reversals - Try `Option > 6 > 5`
6. BugFixes for Stock Data caching with [@swarpatel23](https://github.com/swarpatel23)

## Downloads
* For :desktop_computer: **Windows** users, download **[screenipy.exe](https://github.com/pranjal-joshi/Screeni-py/releases/download/1.33/screenipy.exe)**
* For :penguin: **Linux** users, download **[screenipy.bin](https://github.com/pranjal-joshi/Screeni-py/releases/download/1.33/screenipy.bin)**
* For :apple: **MacOS** users, download **[screenipy.run](https://github.com/pranjal-joshi/Screeni-py/releases/download/1.33/screenipy.run)** ([Read Installation Guide](https://github.com/pranjal-joshi/Screeni-py/blob/main/INSTALLATION.md#for-macos))
* For :desktop_computer: **Windows** users, download **[screenipy.exe](https://github.com/pranjal-joshi/Screeni-py/releases/download/1.34/screenipy.exe)**
* For :penguin: **Linux** users, download **[screenipy.bin](https://github.com/pranjal-joshi/Screeni-py/releases/download/1.34/screenipy.bin)**
* For :apple: **MacOS** users, download **[screenipy.run](https://github.com/pranjal-joshi/Screeni-py/releases/download/1.34/screenipy.run)** ([Read Installation Guide](https://github.com/pranjal-joshi/Screeni-py/blob/main/INSTALLATION.md#for-macos))

## How to use?

Expand Down
6 changes: 1 addition & 5 deletions src/screenipy.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,7 @@ def main(testing=False, testBuild=False, downloadOnly=False):
try:
numStocks = len(listStockCodes)
print(colorText.END+colorText.BOLD)
bar = 'smooth'
spinner = 'waves'
if 'Windows' in platform.platform():
bar = 'classic2'
spinner = 'dots_recur'
bar, spinner = Utility.tools.getProgressbarStyle()
with alive_bar(numStocks, bar=bar, spinner=spinner) as progressbar:
while numStocks:
result = results_queue.get()
Expand Down

0 comments on commit c75db42

Please sign in to comment.