├── 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------