├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── generate_stolgo_rst.bat │ ├── index.rst │ ├── modules.rst │ └── stolgo.rst ├── lib └── stolgo │ ├── __init__.py │ ├── breakout.py │ ├── candlestick.py │ ├── common.py │ ├── exception.py │ ├── helper.py │ ├── nasdaq.py │ ├── request.py │ ├── samco.py │ └── trend.py ├── setup.cfg ├── setup.py ├── stolgo.svg └── tests └── test_nse_option_chain.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present chiranjeev gomatiwal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE 2 | recursive-include lib/stolgo * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | Stolgo is Price Action Trading Analysis Library. Whenever the price reaches resistance during an upward trend, more sellers will enter the market and enter their sell trades. This is a simple price action rule. But How to automate this rule? How to write backtest for this? Stolgo provides APIs for Price Action Trading. 5 | 6 | ## Why Stolgo? 7 | There are many libraries to backtest technical indicators (such as moving average crossover, MACD, RSI, etc.) base strategies, But What about the Price Action Trading? 8 | A Price Action Trader uses support/resistance, candlestick pattern, trend, breakout, and other parameters based on price. You can use Stolgo to backtest your price action trading rules. 9 | 10 | ## Installation 11 | 12 | Use the package manager [pip](https://pip.pypa.io/en/stable/) to install stolgo. 13 | 14 | ```bash 15 | pip install stolgo 16 | ``` 17 | 18 | # For data feed, Stolgo uses [bandl.io](https://bandl.io) , Where by just calling get_data API, You can get data from your favourite broker, directly from exchange website or yahoo finance. 19 | 20 | ## Usage 21 | 22 | ### Get the data, for example using yahoo finance module form [bandl](https://bandl.io) 23 | ```bash 24 | pip install bandl 25 | ``` 26 | 27 | ### Example: Get Indian (NSE/BSE) stock data using Yahoo finance 28 | ```python 29 | from bandl.yfinance import Yfinance 30 | testObj = Yfinance() # returns 'Yfinance class object'. 31 | dfs = testObj.get_data("SBIN",start="21-Jan-2020") #retruns data from 21Jan 2020 to till today 32 | ``` 33 | 34 | ### Example: Get the data of Apple (US Stock) from Nasdaq 35 | ```python 36 | from bandl.nasdaq import Nasdaq 37 | testObj = Nasdaq() # returns 'Nasdaq class object'. 38 | dfs = testObj.get_data("AAPL",periods=90) # returns last 90 days data 39 | ``` 40 | 41 | ### check for bullish engulfing pattern 42 | ```python 43 | from stolgo.candlestick import CandleStick 44 | candle_test = CandleStick() 45 | is_be = candle_test.is_bullish_engulfing(dfs) 46 | ``` 47 | ### check for an inverted hammer candle pattern 48 | ```python 49 | from stolgo.candlestick import CandleStick 50 | candle_test = CandleStick() 51 | is_it = candle_test.is_inverse_hammer_candle(dfs) 52 | ``` 53 | 54 | ### check for breakout 55 | ```python 56 | from stolgo.breakout import Breakout 57 | breakout_test = Breakout() 58 | is_be = breakout_test.is_breaking_out(dfs,periods=None,percentage=None) #periods:Number of candles,percentage: range of consolidation in percentage 59 | ``` 60 | 61 | ## Todo 62 | - Add unittest 63 | - Add more features such as Support Resistance, momemtum, etc. 64 | - Add Event-Driven Backtester 65 | 66 | ## Contributing 67 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 68 | 69 | Kindly follow PEP 8 Coding Style guidelines. Refer: https://www.python.org/dev/peps/pep-0008/ 70 | 71 | Please make sure to update tests as appropriate. 72 | 73 | ## License 74 | [MIT](https://choosealicense.com/licenses/mit/) 75 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('.')) 16 | sys.path.insert(0, os.path.abspath(r'../../lib')) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'stolgo' 22 | copyright = '2020, stolgo developers' 23 | author = 'stolgo developers' 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = '0.1.2' 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.todo', 36 | 'sphinx.ext.viewcode', 37 | 'sphinx.ext.autodoc' 38 | ] 39 | 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # List of patterns, relative to source directory, that match files and 45 | # directories to ignore when looking for source files. 46 | # This pattern also affects html_static_path and html_extra_path. 47 | exclude_patterns = [] 48 | 49 | 50 | # -- Options for HTML output ------------------------------------------------- 51 | 52 | # The theme to use for HTML and HTML Help pages. See the documentation for 53 | # a list of builtin themes. 54 | # 55 | #html_theme = 'alabaster' 56 | html_theme = 'sphinx_rtd_theme' 57 | 58 | # Add any paths that contain custom static files (such as style sheets) here, 59 | # relative to this directory. They are copied after the builtin static files, 60 | # so a file named "default.css" will overwrite the builtin "default.css". 61 | html_static_path = ['_static'] 62 | 63 | master_doc = 'index' -------------------------------------------------------------------------------- /docs/source/generate_stolgo_rst.bat: -------------------------------------------------------------------------------- 1 | sphinx-apidoc -o . ../../lib/stolgo -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. stolgo documentation master file, created by 2 | sphinx-quickstart on Mon May 18 06:00:53 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | stolgo documentation! 7 | ================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | modules 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | stolgo 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | stolgo 8 | -------------------------------------------------------------------------------- /docs/source/stolgo.rst: -------------------------------------------------------------------------------- 1 | stolgo package 2 | ============== 3 | 4 | Submodules 5 | ---------- 6 | 7 | stolgo.helper module 8 | -------------------- 9 | 10 | .. automodule:: stolgo.helper 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | stolgo.nse\_data module 16 | ----------------------- 17 | 18 | .. automodule:: stolgo.nse_data 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | stolgo.nse\_urls module 24 | ----------------------- 25 | 26 | .. automodule:: stolgo.nse_urls 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | stolgo.request module 32 | --------------------- 33 | 34 | .. automodule:: stolgo.request 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | 40 | Module contents 41 | --------------- 42 | 43 | .. automodule:: stolgo 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | -------------------------------------------------------------------------------- /lib/stolgo/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/stolgo/breakout.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from stolgo.exception import BadDataError 3 | 4 | class Breakout: 5 | def __init__(self,periods=13,percentage=2): 6 | self.periods = periods 7 | self.percentage = percentage 8 | 9 | def is_consolidating(self,dfs,periods=None,percentage=None): 10 | """Check if price in consolidating in range for given periods 11 | 12 | :param dfs: input candles 13 | :type dfs: pandas dataframe 14 | :param periods: Number of candles, defaults to None 15 | :type periods: integer, optional 16 | :param percentage: range of consolidation in percentage, defaults to None 17 | :type percentage: float, optional 18 | :raises BadDataError: data error 19 | :return: is_consolidating 20 | :rtype: bool 21 | """ 22 | try: 23 | if not periods: 24 | periods = self.periods 25 | if not percentage: 26 | percentage = self.percentage 27 | 28 | if dfs.shape[0] < periods: 29 | raise BadDataError("Data is not enough for this periods") 30 | recent_dfs = dfs[-1*periods:] 31 | max_close = recent_dfs['Close'].max() 32 | min_close = recent_dfs['Close'].min() 33 | max_adjust = 1-(percentage/100) 34 | if(min_close > max_close * max_adjust): 35 | return True 36 | return False 37 | except Exception as err: 38 | raise Exception(str(err)) 39 | 40 | def is_breaking_out(self,dfs,periods=None,percentage=None): 41 | """check if last candle is breaking out or not 42 | 43 | :param dfs: input candles 44 | :type dfs: pandas dataframe 45 | :param periods: Number of candles, defaults to None 46 | :type periods: integer, optional 47 | :param percentage: range of consolidation in percentage,, defaults to None 48 | :type percentage: float, optional 49 | :raises Exception: data error 50 | :return: is_breaking_out 51 | :rtype: bool 52 | """ 53 | try: 54 | if(self.is_consolidating(dfs[:-1],periods,percentage)): 55 | last_close = dfs[-1:] 56 | recent_dfs = dfs[-1*periods:-1] 57 | if(recent_dfs['Close'].max()last_close['Close'].values[0]): 81 | return True 82 | return False 83 | except Exception as err: 84 | raise Exception(str(err)) 85 | 86 | -------------------------------------------------------------------------------- /lib/stolgo/candlestick.py: -------------------------------------------------------------------------------- 1 | from stolgo.exception import BadDataError 2 | 3 | class CandleStick: 4 | 5 | def is_bearish_candle(self,candle): 6 | return candle["Close"] < candle["Open"] 7 | 8 | def is_bullish_candle(self,candle): 9 | return candle["Close"] > candle["Open"] 10 | 11 | def is_bullish_engulfing(self,candles,pos=-1): 12 | if candles.shape[0] < 2: 13 | raise BadDataError("Minimun two candles require") 14 | curr_candle = candles.iloc[pos] 15 | prev_candle = candles.iloc[pos-1] 16 | 17 | #check for pattern 18 | if (self.is_bearish_candle(prev_candle)\ 19 | and curr_candle["Close"] > prev_candle["Open"] \ 20 | and curr_candle["Open"] = upper_wick*candle_length and candle_lower_wick >= lower_wick*candle_length: 68 | return True 69 | elif self.is_bearish_candle(curr_candle): 70 | candle_body = curr_candle["Open"] - curr_candle["Close"] 71 | candle_upper_wick = curr_candle["High"]-curr_candle["Open"] 72 | candle_lower_wick = curr_candle["Close"]-curr_candle["Low"] 73 | if candle_body <= body * candle_length and candle_upper_wick >= upper_wick*candle_length and candle_lower_wick >= lower_wick*candle_length: 74 | return True 75 | return False -------------------------------------------------------------------------------- /lib/stolgo/common.py: -------------------------------------------------------------------------------- 1 | IND_INDICES = ["BSE CG","SENSEX","BSE CD","NIFTY50 PR 1x INV","BSE IT","METAL", 2 | "OILGAS","NIFTY50 PR 2x LEV","BSEIPO","GREENX","POWER","NIFTY50 TR 1x INV", 3 | "CARBON","BASMTR","CDGS","NIFTY50 TR 2x LEV","BSEFMC","BSE HC","ALLCAP", 4 | "NIFTY50 TR 2x LEV","REALTY","SMEIPO","DOL30","NIFTY Mid LIQ 15","LRGCAP", 5 | "MIDSEL","SMLSEL","NIFTY100 LIQ 15","SNXT50","SNSX50","NIFTY 50","NIFTY Quality 30", 6 | "NIFTY BANK","NIFTY NEXT 50","DOL100","NIFTY MIDCAP 50","NIFTY 100","NIFTY 200", 7 | "NIFTY 500","NIFTY FIN SERVICE","NIFTY AUTO","NIFTY FMCG","NIFTY IT","NIFTY COMMODITIES", 8 | "NIFTY MEDIA","NIFTY METAL","NIFTY PHARMA","NIFTY CONSUMPTION","NIFTY PSU BANK", 9 | "NIFTY PVT BANK","NIFTY REALTY","NIFTY GROWSECT 15","NIFTY CPSE","NIFTY ENERGY", 10 | "NIFTY INFRA","NIFTY DIV OPPS 50","NIFTY MNC","NIFTY PSE","NIFTY SERV SECTOR", 11 | "NIFTY MID100 FREE","DOL200","TECK","BSEPSU","NIFTY SML100 FREE","AUTO","BANKEX", 12 | "INDIA VIX","NIFTY50 VALUE 20" 13 | ] -------------------------------------------------------------------------------- /lib/stolgo/exception.py: -------------------------------------------------------------------------------- 1 | class BadDataError(Exception): 2 | def __init__(self, message): 3 | super(BadDataError, self).__init__(message) -------------------------------------------------------------------------------- /lib/stolgo/helper.py: -------------------------------------------------------------------------------- 1 | from datetime import date as dt 2 | import pandas as pd 3 | import requests 4 | 5 | import stolgo.common 6 | 7 | #default periods 8 | DEFAULT_DAYS = 250 9 | 10 | 11 | def is_ind_index(symbol): 12 | is_it = symbol in stolgo.common.IND_INDICES 13 | return is_it 14 | 15 | def get_formated_date(date=None,format=None,dayfirst=False): 16 | """string date to format date 17 | """ 18 | try: 19 | if not date: 20 | date = dt.today() 21 | date_time = pd.to_datetime(date,dayfirst=dayfirst) 22 | if not format: 23 | format='%m/%d/%Y' 24 | format += ' %H:%M:%S' 25 | 26 | return date_time.strftime(format) 27 | 28 | except Exception as err: 29 | raise Exception("Error occurred while formatting date, Error: ",str(err)) 30 | 31 | def get_formated_dateframe(date=None,format=None,dayfirst=False): 32 | return pd.to_datetime(get_formated_date(date,format,dayfirst),format=format) 33 | 34 | def get_date_offset(periods=None,start=None,end=None,freq="B"): 35 | #use to get start date and end date 36 | if start: 37 | if not periods: 38 | return get_formated_date() 39 | else: 40 | return pd.date_range(start=start,end=end,periods=periods,freq=freq)[-1] 41 | elif end: 42 | return pd.date_range(start=start,end=end,periods=periods,freq=freq)[0] 43 | else: 44 | raise ValueError("start/end , one should be None") 45 | 46 | def get_date_range(start=None,end=None,periods=None,format=None,dayfirst=False,freq="B"): 47 | #Step 1: format date 48 | if start: 49 | start = get_formated_dateframe(start,dayfirst=dayfirst) 50 | if end: 51 | end = get_formated_dateframe(end,dayfirst=dayfirst) 52 | 53 | #Step 2: date range with periods 54 | if (not periods) and (not start): 55 | periods = DEFAULT_DAYS 56 | #if only start, find till today 57 | if start and (not end): 58 | s_from = start 59 | e_till = get_date_offset(start=start,periods=periods)#s_from + pd.offsets.BDay(periods) 60 | #if not start, go to past 61 | elif(end and (not start)): 62 | s_from = get_date_offset(end=end,periods=periods)#e_till - pd.offsets.BDay(periods) 63 | e_till = end 64 | #if start and end, no need to change 65 | elif(start and end): 66 | s_from = start 67 | e_till = end 68 | # if no stat/end and periods given, we get last 1 years of data 69 | else: 70 | e_till = get_formated_dateframe() 71 | s_from = get_date_offset(end=e_till,periods=periods,freq=freq) 72 | 73 | #Step 3: Format to input date format 74 | s_from = get_formated_dateframe(date=s_from,format=format) 75 | e_till = get_formated_dateframe(date=e_till,format=format) 76 | 77 | return s_from,e_till 78 | 79 | def get_data_resample(dfs,time): 80 | dfs.columns = dfs.columns.str.title() 81 | ohlc_dict = {'Open':'first', 'High':'max', 'Low':'min', 'Close': 'last','Volume':'sum'} 82 | return dfs.resample(time,convention="end").agg(ohlc_dict).dropna() -------------------------------------------------------------------------------- /lib/stolgo/nasdaq.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import io 3 | 4 | from datetime import timedelta 5 | import pandas as pd 6 | 7 | from stolgo.helper import get_date_range,get_formated_dateframe 8 | from stolgo.request import RequestUrl,Curl 9 | 10 | #default params for url connection 11 | DEFAULT_TIMEOUT = 5 # seconds 12 | MAX_RETRIES = 2 13 | #default periods 14 | DEFAULT_DAYS = 250 15 | 16 | class NasdaqUrls: 17 | def __init__(self): 18 | self.STK_DATA_PRE_URL = r"https://www.nasdaq.com/api/v1/historical/" 19 | self.date_formats = {"stock_data":"%Y-%m-%d"} 20 | 21 | #historical data header 22 | self.header = { 23 | "authority":"www.nasdaq.com", 24 | "method":"GET", 25 | "path":"/market-activity/stocks/aapl/historical", 26 | "scheme":"https", 27 | "accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 28 | "accept-encoding":"gzip, deflate, br", 29 | "accept-language":"en-GB,en-US;q=0.9,en;q=0.8", 30 | "cache-control":"max-age=0", 31 | "referer":"https://www.nasdaq.com/market-activity/quotes/historical", 32 | "sec-fetch-dest":"document", 33 | "sec-fetch-mode":"navigate", 34 | "sec-fetch-site":"same-origin", 35 | "sec-fetch-user":"?1", 36 | "upgrade-insecure-requests":"1", 37 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36" 38 | } 39 | 40 | def get_data_url(self,symbol,start,end): 41 | try: 42 | start = start.strftime(self.date_formats["stock_data"]) 43 | end = end.strftime(self.date_formats["stock_data"]) 44 | url = self.STK_DATA_PRE_URL + symbol + r"/stocks/" + start + r"/" + end 45 | return url 46 | except Exception as err: 47 | raise Exception("Error occurred in URL constructions ", str(err)) 48 | 49 | class Nasdaq: 50 | """Nasdaq class to get data from nasdaq 51 | """ 52 | def __init__(self,timeout=DEFAULT_TIMEOUT,max_retries=MAX_RETRIES,cloud_mode=False): 53 | if cloud_mode: 54 | self.requests = Curl(timeout,max_retries) 55 | else: 56 | self.requests = RequestUrl(timeout,max_retries) 57 | self.nasdaq_url = NasdaqUrls() 58 | 59 | def __get_data_adjusted(self,dfs,symbol,start=None,end=None,periods=None): 60 | if periods and (dfs.shape[0] < periods): 61 | new_periods = periods - dfs.shape[0] 62 | try: 63 | s_from = e_till = None 64 | #if only start, find till today 65 | if start and (not end): 66 | s_from = dfs.index[0] + timedelta(1) 67 | e_till = None 68 | #if not start, can go to past 69 | elif((end and (not start)) or periods): 70 | s_from = None 71 | e_till = dfs.index[-1] - timedelta(1) 72 | except IndexError as err: 73 | raise Exception("Nasdaq Access error.") 74 | except Exception as exc: 75 | raise Exception("Nasdaq data error: ",str(exc)) 76 | try: 77 | dfs_new = self.get_data(symbol,start = s_from,end = e_till,periods = new_periods) 78 | dfs = self.__join_dfs(dfs,dfs_new).sort_index(ascending=False) 79 | except Exception as exc: 80 | #Small part of data may not be available 81 | pass 82 | return dfs 83 | 84 | def __join_dfs(self,join,joiner): 85 | """will append joiner to join for oi_dfs 86 | 87 | :param join: df which will be appended 88 | :type join: pandas.DataFrame 89 | :param joiner: df which we want to append 90 | :type joiner: pandas.DataFrame 91 | :return: merged data frame 92 | :rtype: pandas.DataFrame 93 | """ 94 | return join.append(joiner) 95 | 96 | def get_data(self,symbol,start=None,end=None,periods=None,dayfirst=False): 97 | """get_data API to fetch data from nasdaq 98 | 99 | :param symbol: stock symbol 100 | :type symbol: string 101 | :param start: start date, defaults to None 102 | :type start: string, optional 103 | :param end: end date, defaults to None 104 | :type end: string, optional 105 | :param periods: number of days, defaults to None 106 | :type periods: integer, optional 107 | :param dayfirst: True if date format is european style DD/MM/YYYY, defaults to False 108 | :type dayfirst: bool, optional 109 | :raises ValueError: for invalid inputs 110 | :raises Exception: incase if no data found 111 | :return: stock data 112 | :rtype: pandas.DataFrame 113 | """ 114 | try: 115 | #Step1: get the date range 116 | s_from,e_till = get_date_range(start=start,end=end,periods=periods,dayfirst=dayfirst) 117 | 118 | if s_from > e_till: 119 | raise ValueError("End should grater than start.") 120 | 121 | url = self.nasdaq_url.get_data_url(symbol=symbol,start=s_from,end=e_till) 122 | res = self.requests.get(url,headers=self.nasdaq_url.header) 123 | 124 | try: 125 | dfs = pd.read_csv(io.StringIO(res.content.decode('utf-8'))) 126 | except Exception as err: 127 | #increase data range, nasdaq not returning for small set 128 | if e_till == get_formated_dateframe(): 129 | raise Exception("Nasdaq not retruning data for this date range.\ 130 | Please, retry with other date ranges") 131 | e_till = get_formated_dateframe() 132 | if (e_till - s_from).days < DEFAULT_DAYS: 133 | s_from = e_till - DEFAULT_DAYS 134 | dfs = self.get_data(symbol,start=s_from,end=e_till) 135 | 136 | dfs.set_index("Date",inplace=True) 137 | #convert to datetime 138 | dfs.index = pd.to_datetime(dfs.index) 139 | dfs = self.__get_data_adjusted(dfs,symbol,start=start,end=end,periods=periods) 140 | return dfs 141 | except Exception as err: 142 | raise Exception("Error occurred while getting data :", str(err)) -------------------------------------------------------------------------------- /lib/stolgo/request.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import requests 4 | from requests.adapters import HTTPAdapter 5 | from urllib3.util import Retry 6 | 7 | DEFAULT_TIMEOUT = 5 # seconds 8 | MAX_RETRIES = 2 9 | 10 | #class TimeoutHTTPAdapter credit : https://github.com/psf/requests/issues/3070#issuecomment-205070203 11 | class TimeoutHTTPAdapter(HTTPAdapter): 12 | def __init__(self, *args, **kwargs): 13 | self.timeout = DEFAULT_TIMEOUT 14 | if "timeout" in kwargs: 15 | self.timeout = kwargs["timeout"] 16 | del kwargs["timeout"] 17 | super().__init__(*args, **kwargs) 18 | 19 | def send(self, request, **kwargs): 20 | timeout = kwargs.get("timeout") 21 | if timeout is None: 22 | kwargs["timeout"] = self.timeout 23 | return super().send(request, **kwargs) 24 | 25 | # If hitting curl with subprocess 26 | class DummyResponse: 27 | def __init__(self,txt): 28 | self.text = None 29 | self.content = None 30 | if txt: 31 | try: 32 | self.text = txt.decode('utf-8') 33 | except UnicodeDecodeError: 34 | self.text = txt.reason.decode('iso-8859-1') 35 | self.content = txt 36 | self.status_code = 200 37 | else: 38 | raise Exception("Could not connect to host") 39 | 40 | class Curl: 41 | def __init__(self,timeout=DEFAULT_TIMEOUT,max_retries=MAX_RETRIES): 42 | self.retry = max_retries 43 | self.timeout = timeout 44 | 45 | def __curl_cmd(self,url,headers): 46 | cmd = 'curl --connect-timeout ' + str(self.timeout) + ' --retry '+ str(self.retry) + ' "' + url + '" ' 47 | query = " -H ".join(['"%s:%s"'%(key,value) for key,value in headers.items()]) 48 | curl_cmd = cmd + "-H " + query + ' --compressed' 49 | return curl_cmd 50 | 51 | def get(self,url,headers): 52 | curl_cmd = self.__curl_cmd(url,headers) 53 | content = subprocess.Popen(curl_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,shell=True) 54 | txt, _ = content.communicate() 55 | try: 56 | response= DummyResponse(txt) 57 | return response 58 | except Exception as err: 59 | raise Exception(str(err)) 60 | 61 | 62 | class RequestUrl: 63 | def __init__(self,timeout=DEFAULT_TIMEOUT,max_retries=MAX_RETRIES): 64 | self.session = self.get_session(timeout=timeout,max_retries=max_retries) 65 | 66 | 67 | def get_session(self,timeout=DEFAULT_TIMEOUT,max_retries=MAX_RETRIES): 68 | #backoff_factor allows us to change how long the processes will sleep between failed requests 69 | retries = Retry( 70 | total=max_retries, 71 | backoff_factor=1, 72 | status_forcelist=[429, 500, 502, 503, 504], 73 | ) 74 | adapter = TimeoutHTTPAdapter(max_retries=retries ,timeout=timeout) 75 | session = requests.Session() 76 | session.mount("https://", adapter) 77 | session.mount("http://", adapter) 78 | return session 79 | 80 | def get(self,*args,**kwargs): 81 | try: 82 | page = self.session.get(*args, **kwargs) 83 | # If the response was successful, no Exception will be raised 84 | page.raise_for_status() 85 | return page 86 | except requests.HTTPError as http_err: 87 | raise Exception("HTTP error occurred while fetching url :", str(http_err.response.content)) 88 | 89 | def post(self,*args,**kwargs): 90 | try: 91 | page = self.session.post(*args, **kwargs) 92 | # If the response was successful, no Exception will be raised 93 | page.raise_for_status() 94 | return page 95 | except requests.HTTPError as http_err: 96 | raise Exception("HTTP error occurred while fetching url :", str(http_err.response.content)) 97 | 98 | -------------------------------------------------------------------------------- /lib/stolgo/samco.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from datetime import datetime,date 4 | import pandas as pd 5 | 6 | from stolgo.helper import get_formated_date,get_date_range,is_ind_index,get_data_resample 7 | from stolgo.request import RequestUrl 8 | 9 | #default params for url connection 10 | DEFAULT_TIMEOUT = 5 # seconds 11 | MAX_RETRIES = 2 12 | 13 | class SamcoUrl: 14 | def __init__(self): 15 | #token needs to be set by user application 16 | self.DATA_HEADER = { 17 | 'Accept': 'application/json', 18 | 'x-session-token': None 19 | } 20 | self.LOGIN_HEADERS = { 21 | 'Content-Type': 'application/json', 22 | 'Accept': 'application/json' 23 | } 24 | self.LOGIN_URL = "https://api.stocknote.com/login" 25 | 26 | #historical data url 27 | self.HIST_INDEX_URL = "https://api.stocknote.com/history/indexCandleData?indexName=" 28 | self.HIST_STK_URL = "https://api.stocknote.com/history/candleData?symbolName=" 29 | 30 | #intraday data 31 | self.INTRA_INDEX_URL = "https://api.stocknote.com/intraday/indexCandleData?indexName=" 32 | self.INTRA_STK_URL = "https://api.stocknote.com/intraday/candleData?symbolName=" 33 | 34 | #option chain url 35 | self.OPTION_CHAIN_URL = "https://api.stocknote.com/option/optionChain" 36 | 37 | self.date_format = { 38 | "HIST":"%Y-%m-%d", 39 | "INTRA":"%Y-%m-%d %H:%M:%S" 40 | } 41 | 42 | def set_session(self,token): 43 | self.DATA_HEADER = { 44 | 'Accept': 'application/json', 45 | 'x-session-token': token 46 | } 47 | 48 | def __build_url(self,symbol,start,end,url,date_format): 49 | symbol = symbol.upper().replace(" ","%20") 50 | start = start.strftime(date_format).replace(" ","%20").replace(":","%3A") 51 | end = end.strftime(date_format).replace(" ","%20").replace(":","%3A") 52 | url_build = url + symbol + "&fromDate=" + start + "&toDate=" + end 53 | return url_build 54 | 55 | def get_intra_data_url(self,symbol,start,end): 56 | try: 57 | url = None 58 | if is_ind_index(symbol): 59 | url = self.__build_url(symbol,start,end,self.INTRA_INDEX_URL,self.date_format["INTRA"]) 60 | else: 61 | url = self.__build_url(symbol,start,end,self.INTRA_STK_URL,self.date_format["INTRA"]) 62 | return url 63 | except Exception as err: 64 | raise Exception("Error occurred while getting stock data URL. ", str(err)) 65 | 66 | def get_hist_data_url(self,symbol,start,end): 67 | try: 68 | url = None 69 | if is_ind_index(symbol): 70 | url = self.__build_url(symbol,start,end,self.HIST_INDEX_URL,self.date_format["HIST"]) 71 | else: 72 | url = self.__build_url(symbol,start,end,self.HIST_STK_URL,self.date_format["HIST"]) 73 | return url 74 | except Exception as err: 75 | raise Exception("Error occurred while getting stock data URL. ", str(err)) 76 | 77 | 78 | class Samco: 79 | def __init__(self,user_id,password,yob,timeout=DEFAULT_TIMEOUT,max_retries=MAX_RETRIES): 80 | #internal initialization 81 | self.__request = RequestUrl(timeout,max_retries) 82 | self.urls = SamcoUrl() 83 | 84 | request_body = { 85 | "userId": user_id, 86 | "password": password, 87 | "yob": yob 88 | } 89 | 90 | #lets login 91 | res = self.__request.post(self.urls.LOGIN_URL, 92 | data=json.dumps(request_body), 93 | headers = self.urls.LOGIN_HEADERS, verify=False) 94 | 95 | self.login_res = json.loads(res.text) 96 | #set token 97 | self.urls.set_session(self.login_res.get("sessionToken")) 98 | 99 | def __get_hist_data(self,symbol,start,end,interval="1D"): 100 | try: 101 | url = self.urls.get_hist_data_url(symbol,start,end) 102 | res = self.__request.get(url,headers=self.urls.DATA_HEADER) 103 | json_key = "historicalCandleData" 104 | if is_ind_index(symbol): 105 | json_key = "indexCandleData" 106 | hist_data_dict = json.loads(res.text).get(json_key) 107 | dfs = pd.json_normalize(hist_data_dict) 108 | dfs.set_index("date",inplace=True) 109 | # Converting the index as date 110 | dfs.index = pd.to_datetime(dfs.index) 111 | return dfs 112 | 113 | except Exception as err: 114 | raise Exception("Error occurred for historical data: ",str(err)) 115 | 116 | def __get_intra_data(self,symbol,start,end,interval="1M"): 117 | try: 118 | url = self.urls.get_intra_data_url(symbol,start,end) 119 | res = self.__request.get(url,headers=self.urls.DATA_HEADER) 120 | json_key = "intradayCandleData" 121 | if is_ind_index(symbol): 122 | json_key = "indexIntraDayCandleData" 123 | intra_data = json.loads(res.text).get(json_key) 124 | dfs = pd.DataFrame(intra_data) 125 | dfs.set_index("dateTime",inplace=True) 126 | # Converting the index as date 127 | dfs.index = pd.to_datetime(dfs.index) 128 | return dfs 129 | except Exception as err: 130 | raise Exception("Error occurred for historical data: ",str(err)) 131 | 132 | def __finetune_df(self,df): 133 | """drop dataframe out of range time 134 | 135 | :param df: input dataframe 136 | :type df: pd.DataFrame 137 | """ 138 | drop_index = (df.between_time("07:00","09:00",include_end=False) + \ 139 | df.between_time("15:30","17:00",include_start=False)).index 140 | df.drop(drop_index,inplace=True) 141 | 142 | def get_data(self,symbol,start=None,end=None,periods=None,interval="1D",dayfirst=False): 143 | """Samco getData API for intraday/Historical data 144 | 145 | :param symbol: stock symbol 146 | :type symbol: string 147 | :param start: start time, defaults to None 148 | :type start: string optional 149 | :param end: end time, defaults to None 150 | :type end: string, optional 151 | :param periods: No of days, defaults to None 152 | :type periods: integer, optional 153 | :param interval: timeframe, defaults to "1D" 154 | :type interval: string, optional 155 | :param dayfirst: if date in european style, defaults to False 156 | :type dayfirst: bool, optional 157 | :raises ValueError: invalid time 158 | :raises Exception: for execption 159 | :return: data requested 160 | :rtype: pandas.DataFrame 161 | """ 162 | try: 163 | s_from,e_till = get_date_range(start=start,end=end,periods=periods,dayfirst=dayfirst) 164 | if s_from > e_till: 165 | raise ValueError("End should grater than start.") 166 | 167 | #capitalize 168 | symbol = symbol.upper() 169 | interval = interval.upper() 170 | 171 | time_frame = pd.Timedelta(interval) 172 | #if interval is 1 day, Use historical data API 173 | day_time_frame = pd.Timedelta("1D") 174 | min_time_frame = pd.Timedelta("1M") 175 | if time_frame >= day_time_frame: 176 | dfs = self.__get_hist_data(symbol,s_from,e_till) 177 | dfs = dfs.apply(pd.to_numeric) 178 | if time_frame != day_time_frame: 179 | dfs = get_data_resample(dfs,interval) 180 | else: 181 | dfs = self.__get_intra_data(symbol,s_from,e_till) 182 | dfs = dfs.apply(pd.to_numeric) 183 | if time_frame != min_time_frame: 184 | dfs = get_data_resample(dfs,interval) 185 | 186 | if not dfs.empty: 187 | return dfs 188 | 189 | except Exception as err: 190 | raise Exception("Error occurred while fetching data :", str(err)) 191 | 192 | def get_optionchain(self,symbol): 193 | params={'searchSymbolName': symbol} 194 | res = self.__request.get(self.urls.OPTION_CHAIN_URL,headers=self.urls.DATA_HEADER,params = params) 195 | return res.json() -------------------------------------------------------------------------------- /lib/stolgo/trend.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import sys 3 | from stolgo.exception import BadDataError 4 | 5 | class Trend: 6 | def __init__(self,periods=13,percentage=2): 7 | self.periods = periods 8 | self.percentage = percentage 9 | 10 | def is_giant_uptrend(self,dfs,periods=None,percentage=None): 11 | """Check if price in consolidating in range for given periods 12 | 13 | :param dfs: input candles 14 | :type dfs: pandas dataframe 15 | :param periods: Number of candles, defaults to None 16 | :type periods: integer, optional 17 | :param percentage: range of consolidation in percentage, defaults to None 18 | :type percentage: float, optional 19 | :raises BadDataError: data error 20 | :return: is_consolidating 21 | :rtype: bool 22 | """ 23 | 24 | try: 25 | if not periods: 26 | periods = self.periods 27 | if not percentage: 28 | percentage = self.percentage 29 | 30 | if dfs.shape[0] < periods: 31 | raise BadDataError("Data is not enough for this periods") 32 | 33 | recent_dfs = dfs[-1*periods:] 34 | prev_candle = None 35 | for candle in recent_dfs.iterrows(): 36 | #check if the candle is green 37 | if(candle[1]["Close"] < candle[1]["Open"]): 38 | return False 39 | if(not prev_candle): 40 | prev_candle = candle 41 | continue 42 | if(prev_candle[1]["Close"] > candle[1]["Close"]): 43 | return False 44 | return True 45 | except Exception as err: 46 | raise Exception(str(err)) 47 | 48 | def is_giant_downtrend(self,dfs,periods=None,percentage=None): 49 | """Check if price in consolidating in range for given periods 50 | 51 | :param dfs: input candles 52 | :type dfs: pandas dataframe 53 | :param periods: Number of candles, defaults to None 54 | :type periods: integer, optional 55 | :param percentage: range of consolidation in percentage, defaults to None 56 | :type percentage: float, optional 57 | :raises BadDataError: data error 58 | :return: is_consolidating 59 | :rtype: bool 60 | """ 61 | 62 | try: 63 | if not periods: 64 | periods = self.periods 65 | if not percentage: 66 | percentage = self.percentage 67 | 68 | if dfs.shape[0] < periods: 69 | raise BadDataError("Data is not enough for this periods") 70 | 71 | recent_dfs = dfs[-1*periods:] 72 | prev_candle = None 73 | for candle in recent_dfs.iterrows(): 74 | #check if the candle is red 75 | if(candle[1]["Close"] > candle[1]["Open"]): 76 | return False 77 | if(not prev_candle): 78 | prev_candle = candle 79 | continue 80 | if(prev_candle[1]["Close"] < candle[1]["Close"]): 81 | return False 82 | return True 83 | except Exception as err: 84 | raise Exception(str(err)) 85 | 86 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [options] 5 | package_dir==lib 6 | packages=find: 7 | where=lib -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup,find_packages 2 | 3 | with open("README.md", 'r') as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name='stolgo', 8 | version='0.1.2.post1', 9 | description='Utilities for the analysis of financial data', 10 | license="MIT", 11 | long_description_content_type='text/markdown', 12 | long_description=long_description, 13 | author='stolgo Developers', 14 | author_email='stockalgos@gmail.com', 15 | project_urls={ 16 | "Organization":"http://www.stolgo.com", 17 | "Source":"https://github.com/stockalgo/stolgo", 18 | "Tracker":"https://github.com/stockalgo/stolgo/issues" 19 | }, 20 | packages=find_packages('lib'), 21 | package_dir = {'':'lib'}, 22 | include_package_data=True, 23 | install_requires=[ 24 | 'requests', 25 | 'pandas', 26 | 'datetime', 27 | 'openpyxl', 28 | 'futures', 29 | 'beautifulsoup4', 30 | 'lxml'], 31 | classifiers=[ 32 | 'Development Status :: 5 - Production/Stable', 33 | 'Intended Audience :: Developers', 34 | 'Topic :: Software Development :: Build Tools', 35 | 'License :: OSI Approved :: MIT License', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.5', 38 | 'Programming Language :: Python :: 3.6', 39 | 'Programming Language :: Python :: 3.7', 40 | 'Programming Language :: Python :: 3.8', 41 | ], 42 | download_url = "https://github.com/stockalgo/stolgo/archive/v_0_1_2.tar.gz", 43 | keywords = ['ALGORITHM', 'NSE', 'STOCK','FINANCE',"DERIVATIVE","NSEDATA"] 44 | ) -------------------------------------------------------------------------------- /stolgo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/test_nse_option_chain.py: -------------------------------------------------------------------------------- 1 | from stolgo.nse_data import NseData 2 | 3 | def main(): 4 | nse_data = NseData() 5 | nse_data.get_option_chain_excel('BANKNIFTY','30APR2020') 6 | 7 | if __name__ == "__main__": 8 | main() 9 | 10 | --------------------------------------------------------------------------------