├── .dockerignore ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.rst ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.rst ├── VERSION ├── docs ├── Makefile ├── conf.py ├── contents.rst ├── index.rst └── make.bat ├── examples ├── example.ipynb └── google_finance.py ├── readthedocs.yml ├── requirements.txt ├── setup.cfg ├── setup.py ├── src └── market_profile │ ├── __init__.py │ └── utils.py ├── tests ├── fixtures │ └── google.csv ├── market_profile_test.py └── utils_test.py └── tox.ini /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .pytest_cache 3 | __pycache__ 4 | .cache 5 | .ipynb_checkpoints 6 | .tox 7 | *.egg-* 8 | *.pyc 9 | dist/ 10 | examples/google.txt 11 | .eggs 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: bfolkens 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .pytest_cache 3 | .cache 4 | .ipynb_checkpoints 5 | .tox 6 | *.egg-* 7 | *.pyc 8 | dist/ 9 | examples/google.txt 10 | .eggs 11 | build/ 12 | docs/_build 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | cache: pip 4 | env: 5 | global: 6 | - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | - SEGFAULT_SIGNALS=all 8 | matrix: 9 | include: 10 | - python: '3.7' 11 | env: 12 | - TOXENV=py3 13 | before_install: 14 | - python --version 15 | - uname -a 16 | - lsb_release -a 17 | install: 18 | - pip install tox 19 | - pip install -r requirements.txt 20 | - virtualenv --version 21 | - easy_install --version 22 | - pip --version 23 | - tox --version 24 | script: 25 | - tox -v 26 | after_failure: 27 | - more .tox/log/* | cat 28 | - more .tox/*/log/* | cat 29 | notifications: 30 | email: 31 | on_success: never 32 | on_failure: always 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Bug reports 9 | =========== 10 | 11 | When `reporting a bug `_ please include: 12 | 13 | * Your operating system name and version. 14 | * Any details about your local setup that might be helpful in troubleshooting. 15 | * Detailed steps to reproduce the bug. 16 | 17 | Documentation improvements 18 | ========================== 19 | 20 | MarketProfile could always use more documentation, whether as part of the 21 | official MarketProfile docs, in docstrings, or even on the web in blog posts, 22 | articles, and such. 23 | 24 | Feature requests and feedback 25 | ============================= 26 | 27 | The best way to send feedback is to file an issue at https://github.com/bfolkens/py-market-profile/issues. 28 | 29 | If you are proposing a feature: 30 | 31 | * Explain in detail how it would work. 32 | * Keep the scope as narrow as possible, to make it easier to implement. 33 | * Remember that this is a volunteer-driven project, and that code contributions are welcome :) 34 | 35 | Development 36 | =========== 37 | 38 | To set up `py-market-profile` for local development: 39 | 40 | 1. Fork `py-market-profile `_ 41 | (look for the "Fork" button). 42 | 2. Clone your fork locally:: 43 | 44 | git clone git@github.com:your_name_here/py-market-profile.git 45 | 46 | 3. Create a branch for local development:: 47 | 48 | git checkout -b name-of-your-bugfix-or-feature 49 | 50 | Now you can make your changes locally. 51 | 52 | 4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: 53 | 54 | tox 55 | 56 | 5. Commit your changes and push your branch to GitHub:: 57 | 58 | git add . 59 | git commit -m "Your detailed description of your changes." 60 | git push origin name-of-your-bugfix-or-feature 61 | 62 | 6. Submit a pull request through the GitHub website. 63 | 64 | Pull Request Guidelines 65 | ----------------------- 66 | 67 | If you need some code review or feedback while you're developing the code just make the pull request. 68 | 69 | For merging, you should: 70 | 71 | 1. Include passing tests (run ``tox``) [1]_. 72 | 2. Update documentation when there's new API, functionality etc. 73 | 3. Add a note to ``CHANGELOG.rst`` about the changes. 74 | 4. Add yourself to ``AUTHORS.rst``. 75 | 76 | .. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will 77 | `run the tests `_ for each change you add in the pull request. 78 | 79 | It will be slower though ... 80 | 81 | Tips 82 | ---- 83 | 84 | To run a subset of tests:: 85 | 86 | tox -e envname -- py.test -k test_myfeature 87 | 88 | To run all the test environments in *parallel* (you need to ``pip install detox``):: 89 | 90 | detox 91 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | WORKDIR /workspace 4 | 5 | COPY requirements.txt ./ 6 | RUN pip install -r requirements.txt 7 | 8 | COPY . ./ 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | Copyright (c) 2017, Brad Folkens 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 7 | following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 10 | disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 16 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 20 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 21 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft src 2 | graft tests 3 | 4 | include LICENSE 5 | include README.rst 6 | include CONTRIBUTING.rst 7 | include VERSION 8 | 9 | include .travis.yml 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Market Profile 3 | ============== 4 | 5 | .. image:: https://api.travis-ci.org/bfolkens/py-market-profile.svg?branch=master 6 | :alt: Travis-CI Build Status 7 | :target: https://travis-ci.org/bfolkens/py-market-profile 8 | 9 | .. image:: https://readthedocs.org/projects/marketprofile/badge/?version=latest 10 | :target: https://marketprofile.readthedocs.io/en/latest/?badge=latest 11 | :alt: Documentation Status 12 | 13 | 14 | A library to calculate Market Profile (Volume Profile) from a Pandas DataFrame. This library expects the DataFrame to have an index of ``timestamp`` and columns for each of the OHLCV values. 15 | 16 | 17 | * Free software: BSD license 18 | 19 | Installation 20 | ============ 21 | 22 | :: 23 | 24 | pip install marketprofile 25 | 26 | Example 27 | ======= 28 | 29 | You can view a Jupyter notebook of an example with charts here: ``_ 30 | 31 | Pull in some data to play with: 32 | 33 | >>> from market_profile import MarketProfile 34 | >>> import pandas_datareader as data 35 | >>> amzn = data.get_data_yahoo('AMZN', '2019-12-01', '2019-12-31') 36 | 37 | Create the MarketProfile object from a Pandas DataFrame: 38 | 39 | >>> mp = MarketProfile(amzn) 40 | >>> mp_slice = mp[amzn.index.min():amzn.index.max()] 41 | 42 | Once you've chosen a slice, you can return the profile series: 43 | 44 | >>> mp_slice.profile 45 | Close 46 | 1739.25 2514300 47 | 1740.50 2823800 48 | 1748.75 2097600 49 | 1749.55 2442800 50 | 1751.60 3117400 51 | 1760.35 3095900 52 | 1760.70 2670100 53 | 1760.95 2745700 54 | 1769.25 3145200 55 | 1770.00 3380900 56 | 1781.60 3925600 57 | 1784.05 3351400 58 | 1786.50 5150800 59 | 1789.25 881300 60 | 1790.70 3644400 61 | 1792.30 2652800 62 | 1793.00 2136400 63 | 1846.90 3674700 64 | 1847.85 2506500 65 | 1868.80 6005400 66 | 1869.85 6186600 67 | Name: Volume, dtype: int64 68 | 69 | Or you can also access individual attributes and properties: 70 | 71 | >>> mp_slice.initial_balance() 72 | (1762.680054, 1805.550049) 73 | 74 | >>> mp_slice.open_range() 75 | (1762.680054, 1805.550049) 76 | 77 | >>> mp_slice.poc_price 78 | 1869.850000 79 | 80 | >>> mp_slice.profile_range 81 | (1739.25, 1869.85) 82 | 83 | >>> mp_slice.value_area 84 | (1760.95, 1869.85) 85 | 86 | >>> mp_slice.balanced_target 87 | 2000.4499999999998 88 | 89 | >>> mp_slice.low_value_nodes 90 | Close 91 | 1748.75 2097600 92 | 1760.70 2670100 93 | 1784.05 3351400 94 | 1789.25 881300 95 | 1793.00 2136400 96 | 1847.85 2506500 97 | Name: Volume, dtype: int64 98 | 99 | >>> mp_slice.high_value_nodes 100 | Close 101 | 1740.5 2823800 102 | 1751.6 3117400 103 | 1781.6 3925600 104 | 1786.5 5150800 105 | 1790.7 3644400 106 | 1846.9 3674700 107 | Name: Volume, dtype: int64 108 | 109 | 110 | Documentation 111 | ============= 112 | 113 | https://marketprofile.readthedocs.io/ 114 | 115 | What is `Market Profile `_ and `How are these calculated `_? 116 | 117 | A discussion on the difference between TPO (Time Price Opportunity) and VOL (Volume Profile) chart types: 118 | ``_ 119 | 120 | Development 121 | =========== 122 | 123 | To run the all tests run:: 124 | 125 | tox 126 | 127 | Development sponsored in part by Cignals, LLC. - Bitcoin `Order Flow and Footprint Charts `_. 128 | 129 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /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 = . 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/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 | import traceback, os, sys 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | sys.path.insert(0, os.path.abspath('../src')) 16 | 17 | with open(os.path.join('..', 'VERSION')) as version_file: 18 | release = version = version_file.read().strip() 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'Market Profile' 24 | year = '2018-2020' 25 | author = 'Brad Folkens' 26 | copyright = '{0}, {1}'.format(year, author) 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.autodoc', 36 | 'sphinx.ext.coverage', 37 | 'sphinx.ext.doctest', 38 | 'sphinx.ext.extlinks' 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 = ['_build', 'Thumbs.db', '.DS_Store'] 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 | 57 | # Add any paths that contain custom static files (such as style sheets) here, 58 | # relative to this directory. They are copied after the builtin static files, 59 | # so a file named "default.css" will overwrite the builtin "default.css". 60 | html_static_path = ['_static'] 61 | 62 | html_show_sourcelink = False 63 | html_show_sphinx = False 64 | html_show_copyright = True 65 | 66 | extlinks = { 67 | 'issue': ('https://github.com/bfolkens/py-market-profile/issues/%s', '#'), 68 | 'pr': ('https://github.com/bfolkens/py-market-profile/pull/%s', 'PR #'), 69 | } 70 | -------------------------------------------------------------------------------- /docs/contents.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfolkens/py-market-profile/a40ad39e3e80ac4f38f13c5de3ad08d86446f1be/docs/contents.rst -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | 4 | .. include:: ../README.rst 5 | 6 | Indices and tables 7 | ================== 8 | 9 | * :ref:`genindex` 10 | * :ref:`modindex` 11 | * :ref:`search` 12 | -------------------------------------------------------------------------------- /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=. 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 | -------------------------------------------------------------------------------- /examples/example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "collapsed": true, 8 | "scrolled": false 9 | }, 10 | "outputs": [], 11 | "source": [ 12 | "%matplotlib inline\n", 13 | "\n", 14 | "import os\n", 15 | "from market_profile import MarketProfile\n", 16 | "from google_finance import *" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "Load google finance data from filesystem, or download and save locally" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 2, 29 | "metadata": { 30 | "collapsed": true 31 | }, 32 | "outputs": [], 33 | "source": [ 34 | "if not os.path.exists('google.txt'):\n", 35 | " get_google_data('google.txt', 'GOOG', 60 * 30, 5)\n", 36 | " \n", 37 | "df = read_google_data('google.txt')" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "Create the MarketProfile based on the dataframe of Google Finance data (OHLCV)" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": 3, 50 | "metadata": { 51 | "collapsed": true 52 | }, 53 | "outputs": [], 54 | "source": [ 55 | "mp = MarketProfile(df, tick_size=1)\n", 56 | "mp_slice = mp[df.index.max() - pd.Timedelta(6.5, 'h'):df.index.max()]" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "Show a nice plot of the calculated profile" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 4, 69 | "metadata": {}, 70 | "outputs": [ 71 | { 72 | "data": { 73 | "text/plain": [ 74 | "" 75 | ] 76 | }, 77 | "execution_count": 4, 78 | "metadata": {}, 79 | "output_type": "execute_result" 80 | }, 81 | { 82 | "data": { 83 | "image/png": "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\nREQ09v8BmFSG6eMZSvYAAAAASUVORK5CYII=\n", 84 | "text/plain": [ 85 | "" 86 | ] 87 | }, 88 | "metadata": {}, 89 | "output_type": "display_data" 90 | } 91 | ], 92 | "source": [ 93 | "data = mp_slice.profile\n", 94 | "data.plot(kind='bar')" 95 | ] 96 | }, 97 | { 98 | "cell_type": "markdown", 99 | "metadata": {}, 100 | "source": [ 101 | "Show some of the indicators that were calculated from the profile above" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 5, 107 | "metadata": {}, 108 | "outputs": [ 109 | { 110 | "name": "stdout", 111 | "output_type": "stream", 112 | "text": [ 113 | "Initial balance: 963.360000, 972.540000\n", 114 | "Opening range: 964.000000, 966.740000\n", 115 | "POC: 978.900000\n", 116 | "Profile range: 964.800000, 978.900000\n", 117 | "Value area: 972.750000, 978.900000\n", 118 | "Balanced Target: 993.000000\n" 119 | ] 120 | } 121 | ], 122 | "source": [ 123 | "print \"Initial balance: %f, %f\" % mp_slice.initial_balance()\n", 124 | "print \"Opening range: %f, %f\" % mp_slice.open_range()\n", 125 | "print \"POC: %f\" % mp_slice.poc_price\n", 126 | "print \"Profile range: %f, %f\" % mp_slice.profile_range\n", 127 | "print \"Value area: %f, %f\" % mp_slice.value_area\n", 128 | "print \"Balanced Target: %f\" % mp_slice.balanced_target" 129 | ] 130 | } 131 | ], 132 | "metadata": { 133 | "kernelspec": { 134 | "display_name": "Python 2", 135 | "language": "python", 136 | "name": "python2" 137 | }, 138 | "language_info": { 139 | "codemirror_mode": { 140 | "name": "ipython", 141 | "version": 2 142 | }, 143 | "file_extension": ".py", 144 | "mimetype": "text/x-python", 145 | "name": "python", 146 | "nbconvert_exporter": "python", 147 | "pygments_lexer": "ipython2", 148 | "version": "2.7.14" 149 | } 150 | }, 151 | "nbformat": 4, 152 | "nbformat_minor": 2 153 | } 154 | -------------------------------------------------------------------------------- /examples/google_finance.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from io import BytesIO 3 | import dateutil.parser 4 | import time 5 | import pandas as pd, numpy as np, datetime 6 | 7 | 8 | def round_float(digits): 9 | def f(x): 10 | return round(np.float32(x), digits) 11 | return f 12 | 13 | def get_google_data(file_path, symbol, interval, days): 14 | url = "http://finance.google.com/finance/getprices?q=%s&i=%d&p=%dd&f=d,o,h,l,c,v" % (symbol, interval, days) 15 | r = requests.get(url) 16 | 17 | with open(file_path, 'wb') as out: 18 | out.write(r.content) 19 | 20 | def read_google_data(file_path): 21 | with open(file_path, 'r') as f: 22 | # Extract meta-info from preamble 23 | for index, line in enumerate(f): 24 | if line.startswith('INTERVAL='): 25 | interval = int(line[9:]) 26 | if index >= 6: 27 | break 28 | 29 | # Reset - read_csv does someting different than reading lines 30 | f.seek(0) 31 | 32 | # Convert the data rows 33 | data = pd.read_csv(f, skiprows=7, header=None, 34 | names=['a', 'Close', 'High', 'Low', 'Open', 'Volume'], 35 | converters={'a' : np.str, 36 | 'Open' : round_float(4), 37 | 'High' : round_float(4), 38 | 'Low' : round_float(4), 39 | 'Close' : round_float(4), 40 | 'Volume' : np.int32 }) 41 | 42 | # Create date sequence for index 43 | for index, row in data.iterrows(): 44 | # Some rows begin with a unix timestamp prefixed by 'a' 45 | # use that to determine the time 46 | if row.a[0] == 'a': 47 | ts = int(row.a.replace('a','')) 48 | t = datetime.datetime.fromtimestamp(ts) 49 | data.loc[index, 'Timestamp'] = t 50 | 51 | # The rest of the rows are intervals from the last timestamp 52 | else: 53 | offset = interval * int(row.a) 54 | data.loc[index, 'Timestamp'] = t + pd.to_timedelta(offset, unit='s') 55 | 56 | # Reformat into a pandas dataframe with the date sequence as an index 57 | data.set_index('Timestamp', inplace=True, drop=True) 58 | data.drop('a', axis=1, inplace=True) 59 | 60 | return data 61 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Build documentation in the docs/ directory with Sphinx 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | # Optionally build your docs in additional formats such as PDF and ePub 12 | formats: all 13 | 14 | # Optionally set the version of Python and requirements required to build your docs 15 | # python: 16 | # version: 3.7 17 | # install: 18 | # - requirements: docs/requirements.txt 19 | 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.13.0 2 | pandas>=0.20.3 3 | scipy>=0.11.0 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = --tb=short 3 | testpaths = tests 4 | python_files = *_test.py 5 | python_classes = *Test 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools import find_packages 3 | 4 | import os 5 | 6 | with open('VERSION') as version_file: 7 | version = version_file.read().strip() 8 | 9 | setup( 10 | name='MarketProfile', 11 | version=version, 12 | author='Brad Folkens', 13 | author_email='bfolkens@gmail.com', 14 | packages=find_packages(where="src"), 15 | package_dir={"": "src"}, 16 | url='https://github.com/bfolkens/py-market-profile', 17 | license='BSD license', 18 | description='A library to calculate Market Profile from a Pandas DataFrame.', 19 | long_description=open('README.rst').read(), 20 | install_requires=[ 21 | "numpy >= 1.13.0", 22 | "pandas >= 0.20.3", 23 | "scipy >= 0.11.0" 24 | ], 25 | setup_requires=['pytest-runner'], 26 | tests_require=['pytest'], 27 | keywords=['python', 'finance', 'quant', 'market profile', 'volume profile'], 28 | classifiers=[ 29 | # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 30 | 'Development Status :: 3 - Alpha', 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: BSD License', 33 | 'Operating System :: Unix', 34 | 'Operating System :: POSIX', 35 | 'Operating System :: Microsoft :: Windows', 36 | 'Programming Language :: Python', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3', 39 | 'Programming Language :: Python :: 3.3', 40 | 'Programming Language :: Python :: 3.4', 41 | 'Programming Language :: Python :: 3.5', 42 | 'Programming Language :: Python :: 3.6', 43 | 'Programming Language :: Python :: 3.7', 44 | 'Programming Language :: Python :: 3.8', 45 | 'Programming Language :: Python :: Implementation :: CPython', 46 | 'Programming Language :: Python :: Implementation :: PyPy', 47 | # uncomment if you test on these interpreters: 48 | # 'Programming Language :: Python :: Implementation :: IronPython', 49 | # 'Programming Language :: Python :: Implementation :: Jython', 50 | # 'Programming Language :: Python :: Implementation :: Stackless', 51 | 'Topic :: Software Development :: Libraries' 52 | ], 53 | ) 54 | -------------------------------------------------------------------------------- /src/market_profile/__init__.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import math 4 | from scipy.signal import argrelextrema 5 | from .utils import midmax_idx 6 | 7 | 8 | class MarketProfile(object): 9 | def __init__(self, df, **kwargs): 10 | self.df = df 11 | self.tick_size = kwargs.pop('tick_size', 0.05) 12 | self.prices_per_row = kwargs.pop('prices_per_row', 1) 13 | self.row_size = kwargs.pop('row_size', self.tick_size * self.prices_per_row) 14 | self.open_range_delta = kwargs.pop('open_range_size', pd.to_timedelta('10 minutes')) 15 | self.initial_balance_delta = kwargs.pop('initial_balance_delta', pd.to_timedelta('1 hour')) 16 | self.value_area_pct = kwargs.pop('value_area_pct', 0.70) 17 | self.mode = kwargs.pop('mode', 'vol') # or tpo 18 | 19 | def __getitem__(self, index): 20 | if isinstance(index, slice): 21 | return MarketProfileSlice(self, slice(index.start, index.stop)) 22 | else: 23 | raise TypeError("index must be int or slice") 24 | 25 | def round_to_row(self, x): 26 | if np.isnan(np.float64(x)): 27 | return x 28 | 29 | roundoff = 1 / float(self.row_size) 30 | return math.ceil(float(x) * roundoff) / roundoff 31 | 32 | 33 | class MarketProfileSlice(object): 34 | def __init__(self, market_profile, _slice): 35 | self.mp = market_profile 36 | self.ds = self.mp.df[_slice] 37 | self.build_profile() 38 | 39 | def open_range(self): 40 | end = self.ds.iloc[0].name + self.mp.open_range_delta 41 | ds = self.ds.loc[:end] 42 | return np.min(ds['Low']), np.max(ds['High']) 43 | 44 | def initial_balance(self): 45 | end = self.ds.iloc[0].name + self.mp.initial_balance_delta 46 | ds = self.ds.loc[:end] 47 | return np.min(ds['Low']), np.max(ds['High']) 48 | 49 | def calculate_value_area(self): 50 | target_vol = self.total_volume * self.mp.value_area_pct 51 | trial_vol = self.poc_volume 52 | 53 | min_idx = self.poc_idx 54 | max_idx = self.poc_idx 55 | 56 | while trial_vol <= target_vol: 57 | last_min = min_idx 58 | last_max = max_idx 59 | 60 | next_min_idx = np.clip(min_idx - 1, 0, len(self.profile) - 1) 61 | next_max_idx = np.clip(max_idx + 1, 0, len(self.profile) - 1) 62 | 63 | low_volume = self.profile.iloc[next_min_idx] if next_min_idx != last_min else None 64 | high_volume = self.profile.iloc[next_max_idx] if next_max_idx != last_max else None 65 | 66 | if not high_volume or (low_volume and low_volume > high_volume): 67 | trial_vol += low_volume 68 | min_idx = next_min_idx 69 | elif not low_volume or (high_volume and low_volume <= high_volume): 70 | trial_vol += high_volume 71 | max_idx = next_max_idx 72 | else: 73 | break 74 | 75 | return self.profile.index[min_idx], self.profile.index[max_idx] 76 | 77 | def calculate_balanced_target(self): 78 | area_above_poc = self.profile.index.max() - self.poc_price 79 | area_below_poc = self.poc_price - self.profile.index.min() 80 | 81 | if area_above_poc >= area_below_poc: 82 | bt = self.poc_price - area_above_poc 83 | else: 84 | bt = self.poc_price + area_below_poc 85 | 86 | return bt 87 | 88 | def find_extrema(self, sign): 89 | (extrema_idx,) = argrelextrema(self.profile.values, sign) 90 | return self.profile.iloc[extrema_idx.tolist()] 91 | 92 | # Calculate the market profile distribution (histogram) 93 | # http://eminimind.com/the-ultimate-guide-to-market-profile/ 94 | def build_profile(self): 95 | rounded_set = self.ds['Close'].apply(lambda x: self.mp.round_to_row(x)) 96 | 97 | if self.mp.mode == 'tpo': 98 | self.profile = self.ds.groupby(rounded_set)['Close'].count() 99 | elif self.mp.mode == 'vol': 100 | self.profile = self.ds.groupby(rounded_set)['Volume'].sum() 101 | else: 102 | raise ValueError("Unrecognized mode: %s" % self.mp.mode) 103 | 104 | self.total_volume = self.profile.sum() 105 | self.profile_range = self.profile.index.min(), self.profile.index.max() 106 | self.poc_idx = midmax_idx(self.profile.values.tolist()) 107 | if self.poc_idx is not None: 108 | self.poc_volume = self.profile.iloc[self.poc_idx] 109 | self.poc_price = self.profile.index[self.poc_idx] 110 | self.value_area = self.calculate_value_area() 111 | self.balanced_target = self.calculate_balanced_target() 112 | else: 113 | self.poc_volume = None 114 | self.poc_price = None 115 | self.value_area = [None, None] 116 | self.balanced_target = None 117 | 118 | self.low_value_nodes = self.find_extrema(np.less) 119 | self.high_value_nodes = self.find_extrema(np.greater) 120 | 121 | def as_dict(self): 122 | ib_low, ib_high = self.initial_balance() 123 | or_low, or_high = self.open_range() 124 | profile_low, profile_high = self.profile_range 125 | val, vah = self.value_area 126 | 127 | return({ 128 | 'or_low': or_low, 129 | 'or_high': or_high, 130 | 'ib_low': ib_low, 131 | 'ib_high': ib_high, 132 | 'poc': self.poc_price, 133 | 'low': profile_low, 134 | 'high': profile_high, 135 | 'val': val, 136 | 'vah': vah, 137 | 'bt': self.balanced_target, 138 | 'lvn': self.low_value_nodes, 139 | 'hvn': self.high_value_nodes 140 | }) 141 | -------------------------------------------------------------------------------- /src/market_profile/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | # Find the index of the maximum closest to middle 5 | def midmax_idx(array): 6 | if len(array) == 0: 7 | return None 8 | 9 | # Find candidate maxima 10 | maxima_idxs = np.argwhere(array == np.amax(array))[:,0] 11 | if len(maxima_idxs) == 1: 12 | return maxima_idxs[0] 13 | elif len(maxima_idxs) <= 1: 14 | return None 15 | 16 | # Find the distances from the midpoint to find 17 | # the maxima with the least distance 18 | midpoint = len(array) / 2 19 | v_norm = np.vectorize(np.linalg.norm) 20 | maximum_idx = np.argmin(v_norm(maxima_idxs - midpoint)) 21 | 22 | return maxima_idxs[maximum_idx] 23 | -------------------------------------------------------------------------------- /tests/fixtures/google.csv: -------------------------------------------------------------------------------- 1 | Timestamp,Close,High,Low,Open,Volume 2 | 2017-09-27 06:30:00,927.74,927.74,927.74,927.74,14929 3 | 2017-09-27 07:00:00,933.0,935.53,928.47,928.745,136776 4 | 2017-09-27 07:30:00,932.365,934.0,930.065,932.98,73146 5 | 2017-09-27 08:00:00,932.39,933.14,930.3,932.45,56624 6 | 2017-09-27 08:30:00,934.8675,935.62,931.1495,932.15,74960 7 | 2017-09-27 09:00:00,934.325,935.96,933.6,935.2308,62358 8 | 2017-09-27 09:30:00,935.48,935.7,933.56,934.36,45043 9 | 2017-09-27 10:00:00,935.5,935.56,933.71,935.385,32434 10 | 2017-09-27 10:30:00,935.96,936.86,935.21,935.5,58203 11 | 2017-09-27 11:00:00,936.08,936.4,935.415,935.88,38559 12 | 2017-09-27 11:30:00,939.81,940.36,936.08,936.12,112876 13 | 2017-09-27 12:00:00,944.2,944.2,939.74,939.8925,117561 14 | 2017-09-27 12:30:00,948.77,949.2499,943.95,944.19,165746 15 | 2017-09-27 13:00:00,944.49,949.9,944.07,948.765,377316 16 | 2017-09-28 06:30:00,941.36,941.36,941.36,941.36,13627 17 | 2017-09-28 07:00:00,947.84,948.1498,940.55,941.63,106773 18 | 2017-09-28 07:30:00,948.17,948.55,944.31,947.7683,54292 19 | 2017-09-28 08:00:00,947.82,950.69,947.71,948.48,68908 20 | 2017-09-28 08:30:00,947.09,948.98,946.55,948.15,26444 21 | 2017-09-28 09:00:00,945.57,947.58,945.0,947.58,33068 22 | 2017-09-28 09:30:00,946.4,946.85,944.95,945.4416,17965 23 | 2017-09-28 10:00:00,946.5,946.83,945.25,946.39,16163 24 | 2017-09-28 10:30:00,948.2104,948.46,945.8,946.46,27849 25 | 2017-09-28 11:00:00,949.22,949.47,947.8,948.185,23687 26 | 2017-09-28 11:30:00,948.0,949.51,947.9,949.195,20906 27 | 2017-09-28 12:00:00,948.78,949.1484,947.49,948.53,20743 28 | 2017-09-28 12:30:00,947.8736,949.21,947.59,948.62,27602 29 | 2017-09-28 13:00:00,949.5,950.08,947.4,947.87,129852 30 | 2017-09-29 06:30:00,952.0,952.0,952.0,952.0,18209 31 | 2017-09-29 07:00:00,955.14,959.7864,951.51,951.51,196489 32 | 2017-09-29 07:30:00,956.425,956.915,953.51,955.25,74709 33 | 2017-09-29 08:00:00,956.9466,958.53,956.07,956.475,46872 34 | 2017-09-29 08:30:00,958.2,959.02,956.59,956.94,60170 35 | 2017-09-29 09:00:00,958.5,958.6,956.01,958.03,52942 36 | 2017-09-29 09:30:00,959.11,959.236,957.47,958.2302,24400 37 | 2017-09-29 10:00:00,958.9816,959.25,958.33,959.1307,28287 38 | 2017-09-29 10:30:00,957.71,959.13,957.43,958.9383,25560 39 | 2017-09-29 11:00:00,958.87,959.405,957.52,957.53,45401 40 | 2017-09-29 11:30:00,959.09,959.28,957.7798,958.9,36429 41 | 2017-09-29 12:00:00,958.7,959.22,958.05,959.07,23219 42 | 2017-09-29 12:30:00,957.8274,959.0,957.74,958.805,58518 43 | 2017-09-29 13:00:00,959.11,959.39,956.07,957.62,335640 44 | 2017-10-02 06:30:00,959.26,959.98,959.26,959.98,24547 45 | 2017-10-02 07:00:00,958.92,962.54,955.371,959.87,112079 46 | 2017-10-02 07:30:00,958.65,960.61,956.97,958.925,57521 47 | 2017-10-02 08:00:00,957.8425,959.535,957.27,958.65,34951 48 | 2017-10-02 08:30:00,956.06,958.07,955.87,957.77,30366 49 | 2017-10-02 09:00:00,949.8964,956.0,949.5,955.99,60363 50 | 2017-10-02 09:30:00,954.0,954.93,947.84,949.44,57387 51 | 2017-10-02 10:00:00,953.8,954.4,952.665,954.045,29613 52 | 2017-10-02 10:30:00,953.67,954.39,953.04,953.53,23593 53 | 2017-10-02 11:00:00,952.5,954.29,952.26,953.6888,34598 54 | 2017-10-02 11:30:00,952.97,953.0,950.815,952.7,35087 55 | 2017-10-02 12:00:00,952.47,953.92,951.32,953.2,33724 56 | 2017-10-02 12:30:00,952.992,953.42,951.56,952.7,69007 57 | 2017-10-02 13:00:00,953.27,953.79,951.49,952.97,238489 58 | 2017-10-03 06:30:00,954.0,954.0,954.0,954.0,8934 59 | 2017-10-03 07:00:00,954.11,956.39,951.7125,953.38,50483 60 | 2017-10-03 07:30:00,951.45,954.53,949.14,954.06,59407 61 | 2017-10-03 08:00:00,953.48,954.46,950.62,951.38,34319 62 | 2017-10-03 08:30:00,952.55,954.05,952.28,953.08,22134 63 | 2017-10-03 09:00:00,951.86,952.55,951.05,952.3,30461 64 | 2017-10-03 09:30:00,953.7124,954.18,951.5528,951.87,41177 65 | 2017-10-03 10:00:00,952.37,954.8,951.89,953.8,31910 66 | 2017-10-03 10:30:00,953.1533,953.19,952.02,952.34,13744 67 | -------------------------------------------------------------------------------- /tests/market_profile_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import pandas as pd 4 | from market_profile import MarketProfile, MarketProfileSlice 5 | 6 | class MarketProfileTest(object): 7 | @pytest.fixture(autouse=True) 8 | def market_profile(self): 9 | df = pd.read_csv( 10 | 'tests/fixtures/google.csv', 11 | index_col=['Timestamp'], 12 | parse_dates=['Timestamp'], 13 | date_parser=pd.to_datetime 14 | ) 15 | return MarketProfile(df, row_size=0.05) 16 | 17 | @pytest.fixture(autouse=True) 18 | def market_profile_slice(self, market_profile): 19 | return market_profile[market_profile.df.index.min():market_profile.df.index.max()] 20 | 21 | def test_round_to_row(self, market_profile): 22 | assert 0.10 == market_profile.round_to_row(0.10) 23 | assert 0.15 == market_profile.round_to_row(0.11) 24 | assert 0.15 == market_profile.round_to_row(0.15) 25 | 26 | def test_create_slice(self, market_profile_slice): 27 | assert isinstance(market_profile_slice, MarketProfileSlice) 28 | 29 | def test_initial_balance(self, market_profile_slice): 30 | assert [927.74, 935.53] == [round(val, 2) for val in market_profile_slice.initial_balance()] 31 | 32 | def test_open_range(self, market_profile_slice): 33 | assert [927.74, 927.74] == [round(val, 2) for val in market_profile_slice.open_range()] 34 | 35 | def test_profile_attributes(self, market_profile_slice): 36 | assert 944.50 == round(market_profile_slice.poc_price, 2) 37 | assert 961.25 == round(market_profile_slice.balanced_target, 2) 38 | assert [927.75, 959.30] == [round(val, 2) for val in market_profile_slice.profile_range] 39 | assert [944.20, 958.95] == [round(val, 2) for val in market_profile_slice.value_area] 40 | 41 | def test_as_dict(self, market_profile_slice): 42 | expected = { 43 | 'or_low': 927.74, 44 | 'or_high': 927.74, 45 | 'ib_low': 927.74, 46 | 'ib_high': 935.53, 47 | 'poc': 944.50, 48 | 'low': 927.75, 49 | 'high': 959.30, 50 | 'val': 944.20, 51 | 'vah': 958.95, 52 | 'bt': 961.25, 53 | } 54 | 55 | computed = market_profile_slice.as_dict() 56 | 57 | for attr in ['or_low', 'or_high', 'ib_low', 'ib_high', 'poc', 'low', 'high', 'val', 'vah', 'bt']: 58 | assert expected[attr] == computed[attr] 59 | 60 | assert True == computed['lvn'].isin([13627, 30366]).any().any() 61 | assert True == computed['hvn'].isin([186489, 238489]).any().any() 62 | -------------------------------------------------------------------------------- /tests/utils_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from market_profile.utils import * 4 | 5 | class UtilsTest(object): 6 | def test_midmax_idx(self): 7 | assert 1 == midmax_idx([1, 4, 2, 3]) 8 | assert 0 == midmax_idx([5140.120268270001, 369.19812357999984]) 9 | assert 0 == midmax_idx([0]) 10 | assert not midmax_idx([]) 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py3 3 | 4 | [testenv] 5 | basepython = 6 | py27: {env:TOXPYTHON:python2.7} 7 | py3: {env:TOXPYTHON:python3.7} 8 | setenv = 9 | PYTHONPATH={toxinidir}/tests 10 | PYTHONUNBUFFERED=yes 11 | passenv = 12 | * 13 | usedevelop = false 14 | deps = 15 | pytest 16 | pytest-travis-fold 17 | -rrequirements.txt 18 | commands = 19 | {posargs:py.test -vv tests} 20 | --------------------------------------------------------------------------------