├── tests ├── __init__.py └── unit │ ├── __init__.py │ └── technical_indicators.py ├── quantpy ├── gui │ ├── __init__.py │ ├── data │ │ ├── app_menu.ui │ │ └── quantpy.ui │ ├── app.py │ ├── settings.py │ └── utils.py ├── fundamental_indicators.py ├── __init__.py ├── event_profiler.py ├── portfolio.py └── technical_indicators.py ├── docs ├── requirements.txt ├── index.rst ├── _templates │ └── layout.html ├── getting_started.rst ├── sphinxext │ ├── numpydoc.py │ ├── ipython_console_highlighting.py │ ├── docscrape_sphinx.py │ ├── inheritance_diagram.py │ ├── docscrape.py │ └── apigen.py ├── make.bat ├── Makefile ├── conf.py └── _static │ └── default.css ├── .gitignore ├── examples ├── efficient_frontier_plot_ex.py ├── nplot_ex.py ├── min_variance_returns_ex.py └── event_ex.py ├── setup.py ├── CHANGES.txt ├── LICENSE └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quantpy/gui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quantpy/fundamental_indicators.py: -------------------------------------------------------------------------------- 1 | def peg(): 2 | pass 3 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | f2py 2 | numpy 3 | matplotlib 4 | pandas 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *pyc 2 | *swp 3 | docs/_build 4 | build 5 | .idea/ 6 | .project 7 | .pydevproject 8 | 9 | -------------------------------------------------------------------------------- /examples/efficient_frontier_plot_ex.py: -------------------------------------------------------------------------------- 1 | # Example plotting effienct frontier. 2 | 3 | import quantpy as qp 4 | 5 | # Grap portfolio 6 | P = qp.Portfolio(['GOOG','IBM','INTC','MSFT','AAPL']) 7 | 8 | # Plot effiecent frontier 9 | P.efficient_frontier_plot() 10 | -------------------------------------------------------------------------------- /quantpy/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from quantpy.portfolio import Portfolio 4 | from quantpy.event_profiler import * 5 | from quantpy.fundamental_indicators import * 6 | from quantpy.technical_indicators import * 7 | 8 | 9 | if sys.version > '3': 10 | long = int 11 | -------------------------------------------------------------------------------- /examples/nplot_ex.py: -------------------------------------------------------------------------------- 1 | # Example plotting returns. 2 | from pylab import * 3 | import quantpy as qp 4 | 5 | # Get symbols. 6 | syms = ['IBM','GOOG','MSFT','AAPL','INTC'] 7 | cols = ['b','r','g','k','m','y'] 8 | 9 | # Get portfolio 10 | P = qp.Portfolio(syms) 11 | 12 | # Make plots of normalized returns. 13 | for sym,col in zip(syms,cols): 14 | P.nplot(sym,col) 15 | show() 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name='QuantPy', 5 | version='0.1', 6 | author='Joseph Smidt', 7 | author_email='josephsmidt@gmail.com', 8 | packages=['quantpy'], 9 | url='https://github.com/jsmidt/QuantPy', 10 | license='LICENSE', 11 | description='A framework for quantitative finance In python', 12 | long_description=open('README.md').read(), 13 | install_requires=[ 14 | "pandas >= 0.10.0", 15 | "matplotlib >= 1.1.0", 16 | ], 17 | ) 18 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. QuantPy documentation master file, created by 2 | sphinx-quickstart on Thu Mar 7 12:45:01 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to QuantPy's documentation! 7 | =================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | getting_started.rst 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /examples/min_variance_returns_ex.py: -------------------------------------------------------------------------------- 1 | from pylab import * 2 | import quantpy as qp 3 | 4 | # Get portfolio. 5 | P = qp.Portfolio(['IBM','GOOG','MSFT','AAPL','INTC']) 6 | 7 | # Calculate the returns buying 1 share of everything. 8 | bb = P.ret_for_w(ones(5)) 9 | cumsum(bb).plot(color='r',label='Buy and Hold Equally.') 10 | mm = cumsum(bb)[-1] 11 | 12 | # Find the optimal weighting that yields the same return with minimum variance. 13 | w = P.min_var_w_ret(mm) 14 | aa = P.ret_for_w(w) 15 | cumsum(aa).plot(label='Same return but min variance.') 16 | legend(loc='best',shadow=True, fancybox=True) 17 | show() 18 | 19 | 20 | -------------------------------------------------------------------------------- /quantpy/gui/data/app_menu.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | _About 8 | app.about 9 | 10 |
11 |
12 | 13 | _Quit 14 | app.quit 15 | <Primary>q 16 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | 4 | {% block rootrellink %} 5 |
  • home
  • 6 |
  • search
  • 7 | {% endblock %} 8 | 9 | 10 | {% block relbar1 %} 11 | 12 |
    13 | py4sci 15 |
    16 | {{ super() }} 17 | {% endblock %} 18 | 19 | {# put the sidebar before the body #} 20 | {% block sidebar1 %}{{ sidebar() }}{% endblock %} 21 | {% block sidebar2 %}{% endblock %} 22 | 23 | -------------------------------------------------------------------------------- /examples/event_ex.py: -------------------------------------------------------------------------------- 1 | import quantpy as qp 2 | from numpy import * 3 | 4 | 5 | # Create your own truth function describing your even. 6 | # In this case the event is: whenever the price chanegs $1. 7 | def truth_function(asset): 8 | truth = zeros(len(asset)) 9 | for i in range(1,len(asset)): 10 | if asset[i] - asset[i-1] > 1.0: 11 | truth[i] = 1 12 | return truth 13 | 14 | 15 | # Grab a profile 16 | P = qp.Portfolio(['GOOG','IBM','INTC']) 17 | 18 | # Define your asset you want to test. 19 | asset = P.asset['IBM']['Adj Close'] 20 | 21 | # Generate your truth function. 22 | truth = truth_function(asset) 23 | 24 | # Get profiles for these events 25 | profiles = qp.event_profiler(asset,truth) 26 | 27 | # Plot them 28 | qp.plot_event_profile(profiles,name='When price increases $1.') 29 | -------------------------------------------------------------------------------- /quantpy/event_profiler.py: -------------------------------------------------------------------------------- 1 | # This concept was inspired by QSTK: 2 | # http://wiki.quantsoftware.org/index.php?title=QuantSoftware_ToolKit. 3 | # However, no source code from that toolkit was used or consulted directly for 4 | # this code. (The concept only) 5 | from pylab import errorbar, xlabel, ylabel, show, legend 6 | from numpy import array, arange 7 | 8 | 9 | def event_profiler(asset, truth, periods=5): 10 | cut = [] 11 | for i in range(periods, len(asset) - periods): 12 | if truth[i] == 1 and asset[i] > 0: 13 | cut.append(asset[i - periods:i + periods] / asset[i]) 14 | return array(cut) 15 | 16 | 17 | def plot_event_profile(events, name=''): 18 | mn = events.mean(axis=0) - 1.0 19 | st = events.std(axis=0) 20 | errorbar(arange(len(mn)), mn, st, label=name) 21 | xlabel('Periods', fontsize=16) 22 | ylabel('Price Change %', fontsize=16) 23 | if len(name) > 1: 24 | legend(loc='best', shadow=True, fancybox=True) 25 | show() 26 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | # Changelog format: 2 | # 3 | # ** Release v ** 4 | # 5 | # Release highlights: 6 | # * Highlight 1 7 | # * Highlight 2 8 | # * Etc... 9 | # Bugs fixed: 10 | # * Fix description 1 (Closes 1234) 11 | # * Fix description 2 (Closes 5678) 12 | # * Etc... 13 | # Miscellaneous notes and known issues: 14 | # * Etc... 15 | # 16 | # git-, , , 17 | # 18 | 19 | ** Release v0.1 ** 20 | 21 | Release Highlights: 22 | * Initial Release 23 | * Portfolio class that can import daily returns from Yahoo. 24 | * Calculation of optimal weights for Sharpe ratio and efficient frontier 25 | * Bare bones event profiler 26 | Bugs fixed: 27 | * Add distutils install like a normal python module enhancement. (Closes 1) 28 | Miscellaneous notes and known issues: 29 | * Please see the "issues" section on Github and jump right in. 30 | 31 | git-4da9cbb, Wed Mar 6 10:42:54, Joseph Smidt 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Joseph Smidt 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 3. All advertising materials mentioning features or use of this software 12 | must display the following acknowledgement: 13 | This product includes software developed by the QuantPy developers. 14 | 4. Neither the name QuantPy nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY Joseph Smidt ''AS IS'' AND ANY EXPRESS OR IMPLIED 19 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 20 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 21 | EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 24 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 25 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /tests/unit/technical_indicators.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | import quantpy as qp 5 | import pandas as pd 6 | 7 | 8 | class SmaTest(unittest.TestCase): 9 | def setUp(self): 10 | self.series = pd.Series((10, 11, 12, 13, 14, 15)) 11 | self.data_frame = pd.DataFrame({ 12 | 'IBM': pd.Series( 13 | [1., 2., 3.], 14 | index=['1/1/13', '2/1/13', '3/1/13'] 15 | ), 16 | 'MSFT': pd.Series( 17 | [1., 1., 2., 8.], 18 | index=['1/1/13', '2/1/13', '3/1/13', '4/1/13'] 19 | ) 20 | }) 21 | 22 | def tearDown(self): 23 | pass 24 | 25 | def test_wrong_window(self): 26 | self.assertRaises(ValueError, qp.sma, self.series, None) 27 | self.assertRaises(ValueError, qp.sma, self.series, '') 28 | self.assertRaises(ValueError, qp.sma, self.series, 'five') 29 | self.assertRaises(ValueError, qp.sma, self.series, -5) 30 | self.assertRaises(ValueError, qp.sma, self.series, -5.5) 31 | self.assertRaises(ValueError, qp.sma, self.series, 0) 32 | self.assertRaises(ValueError, qp.sma, self.series, 5.5) 33 | 34 | def test_wrong_data(self): 35 | import array 36 | self.assertRaises(ValueError, qp.sma, (1, 2, 3), 2) 37 | self.assertRaises(ValueError, qp.sma, [1, 2, 3], 2) 38 | self.assertRaises(ValueError, qp.sma, {1, 2, 3}, 2) 39 | self.assertRaises(ValueError, qp.sma, array.array('i', (1, 2, 3)), 2) 40 | 41 | def test_data(self): 42 | np.testing.assert_array_equal( 43 | pd.Series((np.NaN, np.NaN, 11.0, 12.0, 13.0, 14.0)), 44 | qp.sma(self.series, 3) 45 | ) 46 | 47 | def test_data_frame(self): 48 | df = pd.DataFrame({ 49 | 'IBM': pd.Series( 50 | [np.NaN, 1.5, 2.5, np.NaN], 51 | index=['1/1/13', '2/1/13', '3/1/13', '4/1/13'] 52 | ), 53 | 'MSFT': pd.Series( 54 | [np.NaN, 1.0, 1.5, 5], 55 | index=['1/1/13', '2/1/13', '3/1/13', '4/1/13'] 56 | ) 57 | }) 58 | np.testing.assert_array_equal(df, qp.sma(self.data_frame, 2)) 59 | 60 | if __name__ == '__main__': 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /quantpy/gui/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # 3rd party 5 | from gi.repository import Gio, Gtk 6 | from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as\ 7 | FigureCanvas 8 | from matplotlib.figure import Figure 9 | import pandas as pd 10 | import pylab as pl 11 | 12 | import quantpy as qp 13 | 14 | DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') 15 | 16 | 17 | class App(Gtk.Application): 18 | def __init__(self): 19 | Gtk.Application.__init__(self, application_id='apps.quantpy', 20 | flags=Gio.ApplicationFlags.FLAGS_NONE) 21 | 22 | def do_startup(self): 23 | Gtk.Application.do_startup(self) 24 | 25 | self.builder = Gtk.Builder() 26 | self.builder.add_from_file(os.path.join(DATA_DIR, 'app_menu.ui')) 27 | self.builder.add_from_file(os.path.join(DATA_DIR, "quantpy.ui")) 28 | 29 | self.builder.connect_signals(Handler(self)) 30 | 31 | app_menu = self.builder.get_object('app_menu') 32 | self.set_app_menu(app_menu) 33 | 34 | # todo: look for a way to attach actions automatically, like handlers 35 | about_action = Gio.SimpleAction.new('about', None) 36 | about_action.connect('activate', self.onAppMenuAbout) 37 | self.add_action(about_action) 38 | 39 | quit_action = Gio.SimpleAction.new('quit', None) 40 | quit_action.connect('activate', self.onAppMenuQuit) 41 | self.add_action(quit_action) 42 | 43 | def do_activate(self): 44 | window = self.builder.get_object("window1") 45 | window.set_application(self) 46 | 47 | window.show_all() 48 | 49 | def onAppMenuAbout(self, action, parameter): 50 | self.builder.get_object('aboutdialog1').present() 51 | 52 | def onAppMenuQuit(self, action, parameter): 53 | self.quit() 54 | 55 | 56 | class Handler: 57 | def __init__(self, app): 58 | self.app = app 59 | self.symbol = self.app.builder.get_object('entry1') 60 | 61 | def onDeleteWindow(self, *args): 62 | self.app.quit() 63 | 64 | def onGoPressed(self, button): 65 | symbol = self.symbol.get_text() 66 | asset = pd.io.data.DataReader(symbol, 'yahoo') 67 | 68 | figure = Figure(figsize=(5, 4), dpi=100, frameon=False) 69 | subplot = figure.add_subplot(1, 1, 1) 70 | subplot.plot(asset.index, asset['Adj Close']) 71 | subplot.autoscale_view(True, True, True) 72 | 73 | canvas = FigureCanvas(figure) 74 | canvas.set_size_request(500, 250) 75 | 76 | sw = self.app.builder.get_object('scrolledwindow1') 77 | # remove old children 78 | for child in sw.get_children(): 79 | sw.remove(child) 80 | sw.add(canvas) 81 | sw.show_all() 82 | 83 | def onAboutResponse(self, dialog, response_id): 84 | if response_id in (Gtk.ResponseType.CANCEL, 85 | Gtk.ResponseType.DELETE_EVENT): 86 | dialog.hide_on_delete() 87 | 88 | def onAboutDestroy(self, *args): 89 | # we don't want to destroy the about dialog 90 | return True 91 | 92 | 93 | def main(): 94 | app = App() 95 | status = app.run(sys.argv) 96 | return status 97 | 98 | 99 | if __name__ == "__main__": 100 | sys.exit(main()) 101 | -------------------------------------------------------------------------------- /quantpy/gui/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') 4 | 5 | # database file location 6 | DB_FILE = os.path.join(DATA_DIR, 'db.sqlite3') 7 | 8 | # indexes that are displayed on the dashboard 9 | DASHBOARD_INDEXES = { 10 | #symbol, short name, long name 11 | '^DJI': ('DJIA', 'Dow Jones Industrial Average'), # unfortunately, no data 12 | '^GSPC': ('S&P', 'S&P 500'), 13 | '^IXIC': ('NASDAQ', 'NASDAQ Composite'), 14 | 'eurusd=x': ('EUR/USD', 'EUR/USD'), 15 | '^TNX': ('10-Year Bond', 'CBOE Interest Rate 10-Year T-No'), 16 | 'gcq13.cmx': ('Gold', 'Gold Aug 13'), 17 | 'CLN13.NYM': ('Oil', 'Crude Oil Jul 13'), 18 | } 19 | 20 | 21 | # http://www.gummy-stuff.org/Yahoo-data.htm 22 | YAHOO_SYMBOL_TAGS = { 23 | "a": "Ask", 24 | "a2": "Average Daily Volume", 25 | "a5": "Ask Size", 26 | "b": "Bid", 27 | "b2": "Ask (Real-time)", 28 | "b3": "Bid (Real-time)", 29 | "b4": "Book Value", 30 | "b6": "Bid Size", 31 | "c": "Change & Percent Change", 32 | "c1": "Change", 33 | "c3": "Commission", 34 | "c6": "Change (Real-time)", 35 | "c8": "After Hours Change (Real-time)", 36 | "d": "Dividend/Share", 37 | "d1": "Last Trade Date", 38 | "d2": "Trade Date", 39 | "e": "Earnings/Share", 40 | "e1": "Error Indication (returned for symbol changed / invalid)", 41 | "e7": "EPS Estimate Current Year", 42 | "e8": "EPS Estimate Next Year", 43 | "e9": "EPS Estimate Next Quarter", 44 | "f6": "Float Shares", 45 | "g": "Day's Low", 46 | "g1": "Holdings Gain Percent", 47 | "g3": "Annualized Gain", 48 | "g4": "Holdings Gain", 49 | "g5": "Holdings Gain Percent (Real-time)", 50 | "g6": "Holdings Gain (Real-time)", 51 | "h": "Day's High", 52 | "i": "More Info", 53 | "i5": "Order Book (Real-time)", 54 | "j": "52-week Low", 55 | "j1": "Market Capitalization", 56 | "j3": "Market Cap (Real-time)", 57 | "j4": "EBITDA", 58 | "j5": "Change From 52-week Low", 59 | "j6": "Percent Change From 52-week Low", 60 | "k": "52-week High", 61 | "k1": "Last Trade (Real-time) With Time", 62 | "k2": "Change Percent (Real-time)", 63 | "k3": "Last Trade Size", 64 | "k4": "Change From 52-week High", 65 | "k5": "Percebt Change From 52-week High", 66 | "l": "Last Trade (With Time)", 67 | "l1": "Last Trade (Price Only)", 68 | "l2": "High Limit", 69 | "l3": "Low Limit", 70 | "m": "Day's Range", 71 | "m2": "Day's Range (Real-time)", 72 | "m3": "50-day Moving Average", 73 | "m4": "200-day Moving Average", 74 | "m5": "Change From 200-day Moving Average", 75 | "m6": "Percent Change From 200-day Moving Average", 76 | "m7": "Change From 50-day Moving Average", 77 | "m8": "Percent Change From 50-day Moving Average", 78 | "n": "Name", 79 | "n4": "Notes", 80 | "o": "Open", 81 | "p": "Previous Close", 82 | "p1": "Price Paid", 83 | "p2": "Change in Percent", 84 | "p5": "Price/Sales", 85 | "p6": "Price/Book", 86 | "q": "Ex-Dividend Date", 87 | "r": "P/E Ratio", 88 | "r1": "Dividend Pay Date", 89 | "r2": "P/E Ratio (Real-time)", 90 | "r5": "PEG Ratio", 91 | "r6": "Price/EPS Estimate Current Year", 92 | "r7": "Price/EPS Estimate Next Year", 93 | "s": "Symbol", 94 | "s1": "Shares Owned", 95 | "s7": "Short Ratio", 96 | "t1": "Last Trade Time", 97 | "t6": "Trade Links", 98 | "t7": "Ticker Trend", 99 | "t8": "1 yr Target Price", 100 | "v": "Volume", 101 | "v1": "Holdings Value", 102 | "v7": "Holdings Value (Real-time)", 103 | "w": "52-week Range", 104 | "w1": "Day's Value Change", 105 | "w4": "Day's Value Change (Real-time)", 106 | "x": "Stock Exchange", 107 | "y": "Dividend Yield" 108 | } 109 | -------------------------------------------------------------------------------- /quantpy/gui/utils.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | import urllib.request 3 | 4 | import numpy as np 5 | import pandas as pd 6 | 7 | from quantpy.gui import settings 8 | 9 | 10 | def fetch_news(tickers, kind='company'): 11 | """Download company news headlines from yahoo 12 | 13 | 'tickers' is a comma separated list of tickers: yhoo,msft,tivo 14 | or just one ticker symbol 15 | 16 | 'kind' can either be 'company' or 'industry'. If it's 'industry' industry 17 | (that the company belongs to) news will be fetched. A ValueError is raised 18 | if kind if neither 'company' nor 'industry'. 19 | """ 20 | if not tickers: 21 | return None 22 | 23 | if not kind in ('company', 'industry'): 24 | raise ValueError("'kind' must be one of 'company' or 'industry'.") 25 | 26 | if kind == 'company': 27 | url = 'http://finance.yahoo.com/rss/headline?s=%s' % tickers 28 | else: 29 | url = 'http://finance.yahoo.com/rss/industry?s=%s' % tickers 30 | 31 | feed = urllib.request.urlopen(url) 32 | 33 | tree = ET.parse(feed) 34 | root = tree.getroot() 35 | 36 | news = [] 37 | for item in root.iter('item'): 38 | try: 39 | news.append({ 40 | 'description': item.find('description').text, 41 | 'link': item.find('link').text, 42 | 'pub_date': item.find('pubDate').text, 43 | 'title': item.find('title').text, 44 | }) 45 | except AttributeError: 46 | pass 47 | 48 | return news 49 | 50 | 51 | def get_market_updates(symbols, special_tags): 52 | """ 53 | Get current yahoo quote. 54 | 55 | 'special_tags' is a list of tags. More info about tags can be found at 56 | http://www.gummy-stuff.org/Yahoo-data.htm 57 | 58 | Returns a DataFrame 59 | """ 60 | if isinstance(symbols, str): 61 | sym_list = symbols 62 | elif not isinstance(symbols, pd.Series): 63 | symbols = pd.Series(symbols) 64 | sym_list = str.join('+', symbols) 65 | else: 66 | sym_list = str.join('+', symbols) 67 | 68 | # Symbol must be in the special_tags for now 69 | if not 's' in special_tags: 70 | special_tags.insert(0, 's') 71 | request = ''.join(special_tags) # code request string 72 | special_tag_names = [settings.YAHOO_SYMBOL_TAGS[x] for x in special_tags] 73 | header = special_tag_names 74 | 75 | data = dict(list(zip( 76 | list(special_tag_names), [[] for i in range(len(special_tags))] 77 | ))) 78 | 79 | urlStr = 'http://finance.yahoo.com/d/quotes.csv?s=%s&f=%s' % ( 80 | sym_list, request) 81 | 82 | try: 83 | lines = urllib.request.urlopen(urlStr).readlines() 84 | except Exception as e: 85 | s = "Failed to download:\n{0}".format(e) 86 | print(s) 87 | return None 88 | 89 | for line in lines: 90 | fields = line.decode('utf-8').strip().split(',') 91 | for i, field in enumerate(fields): 92 | if field[-2:] == '%"': 93 | data[header[i]].append(float(field.strip('"%'))) 94 | elif field[0] == '"': 95 | data[header[i]].append(field.strip('"')) 96 | else: 97 | try: 98 | data[header[i]].append(float(field)) 99 | except ValueError: 100 | data[header[i]].append(np.nan) 101 | 102 | idx = data.pop('Symbol') 103 | 104 | return pd.DataFrame(data, index=idx) 105 | 106 | 107 | def get_dashboard_index_updates(): 108 | """Fetch updates for assets in the settings.DASHBOARD_INDEXES 109 | Return a pandas data frame. 110 | """ 111 | symbols = [x for x in settings.DASHBOARD_INDEXES.keys()] 112 | special_tags = ['s', 'c6', 'd1', 'l1', 'p2'] # settings.YAHOO_SYMBOL_TAGS 113 | return get_market_updates(symbols, special_tags) 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | QuantPy 2 | ========== 3 | 4 | A framework for quantitative finance in python. 5 | 6 | **Disclaimer:** This is a *very* alpha project. It is not ready to be used and 7 | won't be for a while. In fact, the author is still very much learning what 8 | such a framework needs to entail. However, if you consider yourself a helpful 9 | soul contributions of any type are more then welcome. Thanks! 10 | 11 | 12 | Some current capabilities: 13 | * Portfolio class that can import daily returns from Yahoo. 14 | * Calculation of optimal weights for Sharpe ratio and efficient frontier 15 | * Bare bones event profiler 16 | 17 | Documentation: 18 | -------------- 19 | 20 | The main documentation can be read at [Read The Docs](https://quantpy.readthedocs.org/en/latest/). Please start their for more information. 21 | 22 | Contributions Welcome. 23 | ---------------------- 24 | 25 | Any and all contributions for the project are welcome whether they be feature 26 | requests, bug reports, contributions to documentation, or patches for new 27 | features, bug fixes of other improvements. Just [fork the 28 | repo](https://help.github.com/articles/fork-a-repo), add some content and [make 29 | a pull request] (https://help.github.com/articles/be-social). If you are new 30 | to Git [this tutorial](http://learn.github.com/p/intro.html) is nice for further 31 | details. 32 | 33 | Also, just downloading the code and providing feedback is also extremely 34 | useful. Submit your feedback to the [issues page 35 | here](https://github.com/jsmidt/QuantPy/issues?state=open). Thanks in advance. 36 | 37 | You may also join us at #quantpy on irc.freenode.net. 38 | 39 | How To Install. 40 | --------------- 41 | 42 | QuantPy may be downloaded from GitHub as:: 43 | 44 | > git clone https://github.com/jsmidt/QuantPy.git 45 | 46 | To install QuantPy type:: 47 | 48 | > cd QuantPy 49 | > python setup.py install 50 | 51 | The prerequisites for Quantpy are: 52 | 53 | * pandas (> 0.10.0) 54 | * matplotlib (> 0.1.0) 55 | * PyGObject (> 3.8.2, only needed for GUI) 56 | 57 | Why Python? 58 | ----------- 59 | 60 | Python is popular, easy to use, cross-platform, contains many helpful 61 | numerical, statistical and visualization libraries and in reality can be made 62 | as fast as C/C++ through Cython extensions. I know of no other language that 63 | meets *all* of these requirements. 64 | 65 | Why BSD license? 66 | ---------------- 67 | 68 | It is a desire that QuantPy is as useful as possible, including for those who 69 | want to incorporate QuantPy into their proprietary software. The BSD license is 70 | an open source license that permits this. Please see the attached LICENSE file 71 | and http://www.linfo.org/bsdlicense.html for more information which states "Due 72 | to the extremely minimal restrictions of BSD-style licenses, software released 73 | under such licenses can be freely modified and used in proprietary (i.e., 74 | commercial) software for which the source code is kept secret." 75 | 76 | Why Git and GitHub? 77 | ------------------- 78 | 79 | There are a few great distributed revision control systems. Git was chosen for 80 | the simple reason that git was designed with the ability to create many 81 | anonymous untracked branches where code can be pushed and pulled from without 82 | revealing the anonymous history. We feel this design choice is important for 83 | entities with proprietary code who want to make contributions but keep their 84 | branches anonymous. Github was chosen because it seems to give the most user 85 | friendly git experience across all platforms: Windows, Mac and Linux. 86 | 87 | 88 | Is Non-Commercial/Proprietary Use allowed? 89 | ------------------------------------------ 90 | 91 | Yes. Though this is an open source project, it was understood from day one 92 | that there may be a need to incorporate QuantPy into proprietary software. The 93 | above sections regarding the BSD licence and the use of Git discuss how we 94 | have addressed these concerns. We hope, however, entities repay the generosity 95 | by submitting patches for new features, bug fixes, and other improvements. 96 | 97 | 98 | With that said, I hope you very much enjoy Quantpy. I hope it meets your 99 | needs, makes you happy and that your retrun the favor through the types of 100 | contributions mentioned above. 101 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. _getting_started: 2 | 3 | 4 | *************** 5 | Getting started 6 | *************** 7 | 8 | Welcome to QuantPy. 9 | 10 | **Disclaimer:** This is a *very* alpha project. It is not ready to be used and 11 | won't be for a while. In fact, the author is still very much learning what 12 | such a framework needs to entail. However, if you consider yourself a helpful 13 | soul contributions of any type are more then welcome. Thanks! 14 | 15 | 16 | Some current capabilities: 17 | * Portfolio class that can import daily returns from Yahoo. 18 | * Calculation of optimal weights for Sharpe ratio and efficient frontier 19 | * Bare bones event profiler 20 | 21 | Contributions Welcome. 22 | ========================= 23 | 24 | Any and all contributions for the project are welcome whether they be feature 25 | requests, bug reports, contributions to documentation, or patches for new 26 | features, bug fixes of other improvements. Just `fork the repo 27 | `_, add some content and `make a 28 | pull request `_. If you are new to 29 | Git `this tutorial `_ is nice for 30 | further details. 31 | 32 | 33 | Also, just downloading the code and providing feedback is also extremely 34 | useful. Submit your feedback to the `issues page here 35 | `_. Thanks in advance. 36 | 37 | 38 | .. _installing-docdir: 39 | 40 | Installing QuantPy 41 | ============================= 42 | 43 | QuantPy may be downloaded from GitHub as:: 44 | 45 | > git clone https://github.com/jsmidt/QuantPy.git 46 | 47 | To install QuantPy type:: 48 | 49 | > cd QuantPy 50 | > python setup.py install 51 | 52 | The prerequisites for Quantpy are: 53 | 54 | * pandas (> 0.10.0) 55 | * matplotlib (> 0.1.0) 56 | 57 | 58 | .. _example-scripts-highlighting-functionality: 59 | 60 | Example Scripts Highlighting Functionality 61 | ============================================== 62 | 63 | Import any portfolio and plot the returns. 64 | ---------------------------------------------- 65 | 66 | The example script imports a portfolio of stocks and plots the normalized retuns: 67 | 68 | .. plot:: ../examples/nplot_ex.py 69 | :include-source: 70 | 71 | Calculate the weighting that gives minimum variance. 72 | ------------------------------------------------------ 73 | 74 | QuantPy can tell you the portfolio weighting that will give you the minimum 75 | variance. (Max sharpe ratio) This is illustrated in the 76 | min_variance_returns_ex.py script where you compare how your portfolio would 77 | have changed if you bought an equal number of all stocks in the portfolio 78 | versus used optimal sharpe ratio weighting for the same return. 79 | 80 | .. plot:: ../examples/min_variance_returns_ex.py 81 | :include-source: 82 | 83 | Plot the Efficient Frontier. 84 | ------------------------------- 85 | 86 | We can plot the entire efficient frontier as done in 87 | efficient_frontier_plot_ex.py. This is defined as followed: you tell me what 88 | returns you want and this will tell you the weighting that will generate those 89 | returns with minimum variance. The relation or risk to return for such a 90 | weighting is plotted in efficient_frontier.png. The line showing the optimal 91 | return for risk (optimal share ratio) is also plotted. 92 | 93 | .. plot:: ../examples/efficient_frontier_plot_ex.py 94 | :include-source: 95 | 96 | Using an Event Profiler 97 | ------------------------- 98 | 99 | Event profiler allows you to track what happens to a stock price after 100 | historical events. Tell me any event, like an EMA cross, and this will plot 101 | how the price historically has changed with such an event with error bars. 102 | This helps us find statistically meaningful events. 103 | 104 | Below is a bare bones version of this demonstrated by running event_ex.py. It 105 | just asks what happens to the price after it goes up at least $1. As the plot 106 | event_ex.png shows, nothing meaningful. Notice you must create your own "truth function" describing your event. 107 | 108 | .. plot:: ../examples/event_ex.py 109 | :include-source: 110 | 111 | Thanks Again! 112 | ================= 113 | 114 | We want to thank you for trying out QuantPy. Any contributions are again very appreciated. 115 | 116 | -------------------------------------------------------------------------------- /docs/sphinxext/numpydoc.py: -------------------------------------------------------------------------------- 1 | """ 2 | ======== 3 | numpydoc 4 | ======== 5 | 6 | Sphinx extension that handles docstrings in the Numpy standard format. [1] 7 | 8 | It will: 9 | 10 | - Convert Parameters etc. sections to field lists. 11 | - Convert See Also section to a See also entry. 12 | - Renumber references. 13 | - Extract the signature from the docstring, if it can't be determined otherwise. 14 | 15 | .. [1] http://projects.scipy.org/scipy/numpy/wiki/CodingStyleGuidelines#docstring-standard 16 | 17 | """ 18 | 19 | import os, re, pydoc 20 | from docscrape_sphinx import get_doc_object, SphinxDocString 21 | import inspect 22 | 23 | def mangle_docstrings(app, what, name, obj, options, lines, 24 | reference_offset=[0]): 25 | if what == 'module': 26 | # Strip top title 27 | title_re = re.compile(r'^\s*[#*=]{4,}\n[a-z0-9 -]+\n[#*=]{4,}\s*', 28 | re.I|re.S) 29 | lines[:] = title_re.sub('', "\n".join(lines)).split("\n") 30 | else: 31 | doc = get_doc_object(obj, what, "\n".join(lines)) 32 | lines[:] = str(doc).split("\n") 33 | 34 | if app.config.numpydoc_edit_link and hasattr(obj, '__name__') and \ 35 | obj.__name__: 36 | if hasattr(obj, '__module__'): 37 | v = dict(full_name="%s.%s" % (obj.__module__, obj.__name__)) 38 | else: 39 | v = dict(full_name=obj.__name__) 40 | lines += ['', '.. htmlonly::', ''] 41 | lines += [' %s' % x for x in 42 | (app.config.numpydoc_edit_link % v).split("\n")] 43 | 44 | # replace reference numbers so that there are no duplicates 45 | references = [] 46 | for l in lines: 47 | l = l.strip() 48 | if l.startswith('.. ['): 49 | try: 50 | references.append(int(l[len('.. ['):l.index(']')])) 51 | except ValueError: 52 | print "WARNING: invalid reference in %s docstring" % name 53 | 54 | # Start renaming from the biggest number, otherwise we may 55 | # overwrite references. 56 | references.sort() 57 | if references: 58 | for i, line in enumerate(lines): 59 | for r in references: 60 | new_r = reference_offset[0] + r 61 | lines[i] = lines[i].replace('[%d]_' % r, 62 | '[%d]_' % new_r) 63 | lines[i] = lines[i].replace('.. [%d]' % r, 64 | '.. [%d]' % new_r) 65 | 66 | reference_offset[0] += len(references) 67 | 68 | def mangle_signature(app, what, name, obj, options, sig, retann): 69 | # Do not try to inspect classes that don't define `__init__` 70 | if (inspect.isclass(obj) and 71 | 'initializes x; see ' in pydoc.getdoc(obj.__init__)): 72 | return '', '' 73 | 74 | if not (callable(obj) or hasattr(obj, '__argspec_is_invalid_')): return 75 | if not hasattr(obj, '__doc__'): return 76 | 77 | doc = SphinxDocString(pydoc.getdoc(obj)) 78 | if doc['Signature']: 79 | sig = re.sub("^[^(]*", "", doc['Signature']) 80 | return sig, '' 81 | 82 | def initialize(app): 83 | try: 84 | app.connect('autodoc-process-signature', mangle_signature) 85 | except: 86 | monkeypatch_sphinx_ext_autodoc() 87 | 88 | def setup(app, get_doc_object_=get_doc_object): 89 | global get_doc_object 90 | get_doc_object = get_doc_object_ 91 | 92 | app.connect('autodoc-process-docstring', mangle_docstrings) 93 | app.connect('builder-inited', initialize) 94 | app.add_config_value('numpydoc_edit_link', None, True) 95 | 96 | #------------------------------------------------------------------------------ 97 | # Monkeypatch sphinx.ext.autodoc to accept argspecless autodocs (Sphinx < 0.5) 98 | #------------------------------------------------------------------------------ 99 | 100 | def monkeypatch_sphinx_ext_autodoc(): 101 | global _original_format_signature 102 | import sphinx.ext.autodoc 103 | 104 | if sphinx.ext.autodoc.format_signature is our_format_signature: 105 | return 106 | 107 | print "[numpydoc] Monkeypatching sphinx.ext.autodoc ..." 108 | _original_format_signature = sphinx.ext.autodoc.format_signature 109 | sphinx.ext.autodoc.format_signature = our_format_signature 110 | 111 | def our_format_signature(what, obj): 112 | r = mangle_signature(None, what, None, obj, None, None, None) 113 | if r is not None: 114 | return r[0] 115 | else: 116 | return _original_format_signature(what, obj) 117 | -------------------------------------------------------------------------------- /docs/sphinxext/ipython_console_highlighting.py: -------------------------------------------------------------------------------- 1 | """reST directive for syntax-highlighting ipython interactive sessions. 2 | 3 | XXX - See what improvements can be made based on the new (as of Sept 2009) 4 | 'pycon' lexer for the python console. At the very least it will give better 5 | highlighted tracebacks. 6 | """ 7 | 8 | #----------------------------------------------------------------------------- 9 | # Needed modules 10 | 11 | # Standard library 12 | import re 13 | 14 | # Third party 15 | from pygments.lexer import Lexer, do_insertions 16 | from pygments.lexers.agile import (PythonConsoleLexer, PythonLexer, 17 | PythonTracebackLexer) 18 | from pygments.token import Comment, Generic 19 | 20 | from sphinx import highlighting 21 | 22 | #----------------------------------------------------------------------------- 23 | # Global constants 24 | line_re = re.compile('.*?\n') 25 | 26 | #----------------------------------------------------------------------------- 27 | # Code begins - classes and functions 28 | 29 | class IPythonConsoleLexer(Lexer): 30 | """ 31 | For IPython console output or doctests, such as: 32 | 33 | .. sourcecode:: ipython 34 | 35 | In [1]: a = 'foo' 36 | 37 | In [2]: a 38 | Out[2]: 'foo' 39 | 40 | In [3]: print a 41 | foo 42 | 43 | In [4]: 1 / 0 44 | 45 | Notes: 46 | 47 | - Tracebacks are not currently supported. 48 | 49 | - It assumes the default IPython prompts, not customized ones. 50 | """ 51 | 52 | name = 'IPython console session' 53 | aliases = ['ipython'] 54 | mimetypes = ['text/x-ipython-console'] 55 | input_prompt = re.compile("(In \[[0-9]+\]: )|( \.\.\.+:)") 56 | output_prompt = re.compile("(Out\[[0-9]+\]: )|( \.\.\.+:)") 57 | continue_prompt = re.compile(" \.\.\.+:") 58 | tb_start = re.compile("\-+") 59 | 60 | def get_tokens_unprocessed(self, text): 61 | pylexer = PythonLexer(**self.options) 62 | tblexer = PythonTracebackLexer(**self.options) 63 | 64 | curcode = '' 65 | insertions = [] 66 | for match in line_re.finditer(text): 67 | line = match.group() 68 | input_prompt = self.input_prompt.match(line) 69 | continue_prompt = self.continue_prompt.match(line.rstrip()) 70 | output_prompt = self.output_prompt.match(line) 71 | if line.startswith("#"): 72 | insertions.append((len(curcode), 73 | [(0, Comment, line)])) 74 | elif input_prompt is not None: 75 | insertions.append((len(curcode), 76 | [(0, Generic.Prompt, input_prompt.group())])) 77 | curcode += line[input_prompt.end():] 78 | elif continue_prompt is not None: 79 | insertions.append((len(curcode), 80 | [(0, Generic.Prompt, continue_prompt.group())])) 81 | curcode += line[continue_prompt.end():] 82 | elif output_prompt is not None: 83 | # Use the 'error' token for output. We should probably make 84 | # our own token, but error is typicaly in a bright color like 85 | # red, so it works fine for our output prompts. 86 | insertions.append((len(curcode), 87 | [(0, Generic.Error, output_prompt.group())])) 88 | curcode += line[output_prompt.end():] 89 | else: 90 | if curcode: 91 | for item in do_insertions(insertions, 92 | pylexer.get_tokens_unprocessed(curcode)): 93 | yield item 94 | curcode = '' 95 | insertions = [] 96 | yield match.start(), Generic.Output, line 97 | if curcode: 98 | for item in do_insertions(insertions, 99 | pylexer.get_tokens_unprocessed(curcode)): 100 | yield item 101 | 102 | 103 | def setup(app): 104 | """Setup as a sphinx extension.""" 105 | 106 | # This is only a lexer, so adding it below to pygments appears sufficient. 107 | # But if somebody knows that the right API usage should be to do that via 108 | # sphinx, by all means fix it here. At least having this setup.py 109 | # suppresses the sphinx warning we'd get without it. 110 | pass 111 | 112 | #----------------------------------------------------------------------------- 113 | # Register the extension as a valid pygments lexer 114 | highlighting.lexers['ipython'] = IPythonConsoleLexer() 115 | -------------------------------------------------------------------------------- /docs/sphinxext/docscrape_sphinx.py: -------------------------------------------------------------------------------- 1 | import re, inspect, textwrap, pydoc 2 | from docscrape import NumpyDocString, FunctionDoc, ClassDoc 3 | 4 | class SphinxDocString(NumpyDocString): 5 | # string conversion routines 6 | def _str_header(self, name, symbol='`'): 7 | return ['.. rubric:: ' + name, ''] 8 | 9 | def _str_field_list(self, name): 10 | return [':' + name + ':'] 11 | 12 | def _str_indent(self, doc, indent=4): 13 | out = [] 14 | for line in doc: 15 | out += [' '*indent + line] 16 | return out 17 | 18 | def _str_signature(self): 19 | return [''] 20 | if self['Signature']: 21 | return ['``%s``' % self['Signature']] + [''] 22 | else: 23 | return [''] 24 | 25 | def _str_summary(self): 26 | return self['Summary'] + [''] 27 | 28 | def _str_extended_summary(self): 29 | return self['Extended Summary'] + [''] 30 | 31 | def _str_param_list(self, name): 32 | out = [] 33 | if self[name]: 34 | out += self._str_field_list(name) 35 | out += [''] 36 | for param,param_type,desc in self[name]: 37 | out += self._str_indent(['**%s** : %s' % (param.strip(), 38 | param_type)]) 39 | out += [''] 40 | out += self._str_indent(desc,8) 41 | out += [''] 42 | return out 43 | 44 | def _str_section(self, name): 45 | out = [] 46 | if self[name]: 47 | out += self._str_header(name) 48 | out += [''] 49 | content = textwrap.dedent("\n".join(self[name])).split("\n") 50 | out += content 51 | out += [''] 52 | return out 53 | 54 | def _str_see_also(self, func_role): 55 | out = [] 56 | if self['See Also']: 57 | see_also = super(SphinxDocString, self)._str_see_also(func_role) 58 | out = ['.. seealso::', ''] 59 | out += self._str_indent(see_also[2:]) 60 | return out 61 | 62 | def _str_warnings(self): 63 | out = [] 64 | if self['Warnings']: 65 | out = ['.. warning::', ''] 66 | out += self._str_indent(self['Warnings']) 67 | return out 68 | 69 | def _str_index(self): 70 | idx = self['index'] 71 | out = [] 72 | if len(idx) == 0: 73 | return out 74 | 75 | out += ['.. index:: %s' % idx.get('default','')] 76 | for section, references in idx.iteritems(): 77 | if section == 'default': 78 | continue 79 | elif section == 'refguide': 80 | out += [' single: %s' % (', '.join(references))] 81 | else: 82 | out += [' %s: %s' % (section, ','.join(references))] 83 | return out 84 | 85 | def _str_references(self): 86 | out = [] 87 | if self['References']: 88 | out += self._str_header('References') 89 | if isinstance(self['References'], str): 90 | self['References'] = [self['References']] 91 | out.extend(self['References']) 92 | out += [''] 93 | return out 94 | 95 | def __str__(self, indent=0, func_role="obj"): 96 | out = [] 97 | out += self._str_signature() 98 | out += self._str_index() + [''] 99 | out += self._str_summary() 100 | out += self._str_extended_summary() 101 | for param_list in ('Parameters', 'Attributes', 'Methods', 102 | 'Returns','Raises'): 103 | out += self._str_param_list(param_list) 104 | out += self._str_warnings() 105 | out += self._str_see_also(func_role) 106 | out += self._str_section('Notes') 107 | out += self._str_references() 108 | out += self._str_section('Examples') 109 | out = self._str_indent(out,indent) 110 | return '\n'.join(out) 111 | 112 | class SphinxFunctionDoc(SphinxDocString, FunctionDoc): 113 | pass 114 | 115 | class SphinxClassDoc(SphinxDocString, ClassDoc): 116 | pass 117 | 118 | def get_doc_object(obj, what=None, doc=None): 119 | if what is None: 120 | if inspect.isclass(obj): 121 | what = 'class' 122 | elif inspect.ismodule(obj): 123 | what = 'module' 124 | elif callable(obj): 125 | what = 'function' 126 | else: 127 | what = 'object' 128 | if what == 'class': 129 | return SphinxClassDoc(obj, '', func_doc=SphinxFunctionDoc, doc=doc) 130 | elif what in ('function', 'method'): 131 | return SphinxFunctionDoc(obj, '', doc=doc) 132 | else: 133 | if doc is None: 134 | doc = pydoc.getdoc(obj) 135 | return SphinxDocString(doc) 136 | 137 | -------------------------------------------------------------------------------- /quantpy/gui/data/quantpy.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | False 6 | QuantPy 7 | 8 | 9 | 10 | True 11 | False 12 | 10 13 | 10 14 | 10 15 | 10 16 | vertical 17 | 18 | 19 | True 20 | False 21 | 5 22 | 23 | 24 | True 25 | False 26 | 0.47999998927116394 27 | Symbol 28 | 29 | 30 | False 31 | True 32 | 0 33 | 34 | 35 | 36 | 37 | True 38 | True 39 | 40 | 8 41 | 42 | 43 | False 44 | True 45 | 1 46 | 47 | 48 | 49 | 50 | Go 51 | True 52 | True 53 | True 54 | 55 | 56 | 57 | False 58 | True 59 | 2 60 | 61 | 62 | 63 | 64 | False 65 | True 66 | 0 67 | 68 | 69 | 70 | 71 | 500 72 | 250 73 | True 74 | True 75 | 76 | 77 | 78 | 79 | 80 | True 81 | True 82 | 1 83 | 84 | 85 | 86 | 87 | 88 | 89 | False 90 | 5 91 | True 92 | center-on-parent 93 | dialog 94 | window1 95 | QuantPy 96 | https://github.com/jsmidt/QuantPy 97 | 98 | 99 | 100 | 101 | False 102 | vertical 103 | 2 104 | 105 | 106 | False 107 | end 108 | 109 | 110 | False 111 | True 112 | end 113 | 0 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /quantpy/portfolio.py: -------------------------------------------------------------------------------- 1 | from pandas.io.data import DataReader 2 | from pylab import legend, xlabel, ylabel, sqrt, ylim,\ 3 | cov, sqrt, mean, std, plot, show, figure 4 | from numpy import array, zeros, matrix, ones, shape, linspace, hstack 5 | from pandas import Series, DataFrame 6 | from numpy.linalg import inv 7 | 8 | 9 | # Portfolio class. 10 | class Portfolio: 11 | def __init__(self, symbols, start=None, end=None, bench='^GSPC'): 12 | 13 | # Make sure input is a list 14 | if type(symbols) != list: 15 | symbols = [symbols] 16 | 17 | # Create distionary to hold assets. 18 | self.asset = {} 19 | 20 | # Retrieve assets from data source (IE. Yahoo) 21 | for symbol in symbols: 22 | try: 23 | self.asset[symbol] = DataReader( 24 | symbol, "yahoo", start=start, end=end) 25 | except: 26 | print("Asset " + str(symbol) + " not found!") 27 | 28 | # Get Benchmark asset. 29 | self.benchmark = DataReader(bench, "yahoo", start=start, end=end) 30 | self.benchmark['Return'] = self.benchmark['Adj Close'].diff() 31 | 32 | # Get returns, beta, alpha, and sharp ratio. 33 | for symbol in symbols: 34 | # Get returns. 35 | self.asset[symbol]['Return'] = \ 36 | self.asset[symbol]['Adj Close'].diff() 37 | # Get Beta. 38 | A = self.asset[symbol]['Return'].fillna(0) 39 | B = self.benchmark['Return'].fillna(0) 40 | self.asset[symbol]['Beta'] = cov(A, B)[0, 1] / cov(A, B)[1, 1] 41 | # Get Alpha 42 | self.asset[symbol]['Alpha'] = self.asset[symbol]['Return'] - \ 43 | self.asset[symbol]['Beta'] * self.benchmark['Return'] 44 | 45 | # Get Sharpe Ratio 46 | tmp = self.asset[symbol]['Return'] 47 | self.asset[symbol]['Sharpe'] = \ 48 | sqrt(len(tmp)) * mean(tmp.fillna(0)) / std(tmp.fillna(0)) 49 | 50 | def nplot(self, symbol, color='b', nval=0): 51 | tmp = (self.benchmark if symbol == 'bench' else self.asset[symbol]) ['Adj Close'] 52 | tmp = tmp / tmp[nval] 53 | tmp.plot(color=color, label=symbol) 54 | legend(loc='best', shadow=True, fancybox=True) 55 | 56 | def betas(self): 57 | betas = [v['Beta'][0] for k, v in self.asset.items()] 58 | return Series(betas, index=self.asset.keys()) 59 | 60 | def returns(self): 61 | returns = [v['Return'].dropna() for k, v in self.asset.items()] 62 | return Series(returns, index=self.asset.keys()) 63 | 64 | def cov(self): 65 | keys, values = self.returns().keys(), self.returns().values() 66 | return DataFrame( 67 | cov(array(values)), index=keys, columns=keys) 68 | 69 | def get_w(self, kind='sharpe'): 70 | V = self.cov() 71 | iV = matrix(inv(V)) 72 | 73 | if kind == 'characteristic': 74 | e = matrix(ones(len(self.asset.keys()))).T 75 | elif kind == 'sharpe': 76 | suml = [ self.returns()[symbol].sum() for symbol in self.asset.keys()] 77 | e = matrix(suml).T 78 | else: 79 | print('\n *Error: There is no weighting for kind ' + kind) 80 | return 81 | 82 | num = iV * e 83 | denom = e.T * iV * e 84 | w = array(num / denom).flatten() 85 | return Series(w, index=self.asset.keys()) 86 | 87 | def efficient_frontier_w(self, fp): 88 | wc = self.get_w(kind='characteristic') 89 | wq = self.get_w(kind='sharpe') 90 | 91 | fc = self.ret_for_w(wc).sum() 92 | fq = self.ret_for_w(wq).sum() 93 | 94 | denom = fq - fc 95 | w = (fq - fp) * wc + (fp - fc) * wq 96 | return Series(w / denom, index=self.asset.keys()) 97 | 98 | def efficient_frontier(self, xi=0.01, xf=4, npts=100, scale=10): 99 | frontier = linspace(xi, xf, npts) 100 | 101 | i = 0 102 | rets = zeros(len(frontier)) 103 | sharpe = zeros(len(frontier)) 104 | for f in frontier: 105 | w = self.efficient_frontier_w(f) 106 | tmp = self.ret_for_w(w) 107 | rets[i] = tmp.sum() * scale 108 | sharpe[i] = mean(tmp) / std(tmp) * sqrt(len(tmp)) 109 | i += 1 110 | risk = rets/sharpe 111 | return Series(rets, index=risk), sharpe.max() 112 | 113 | def efficient_frontier_plot(self, xi=0.01, xf=4, npts=100, scale=0.1, 114 | col1='b', col2='r', newfig=1, plabel=''): 115 | eff, m = self.efficient_frontier() 116 | 117 | if newfig == 1: 118 | figure() 119 | 120 | plot(array(eff.index), array(eff), col1, linewidth=2, 121 | label="Efficient Frontier " + plabel) 122 | tmp = zeros(1) 123 | plot(hstack((tmp, array(eff.index))), 124 | hstack((0, m * array(eff.index))), 125 | col2, linewidth=2, label="Max Sharpe Ratio: %6.2g" % m) 126 | legend(loc='best', shadow=True, fancybox=True) 127 | xlabel('Risk %', fontsize=16) 128 | ylabel('Return %', fontsize=16) 129 | show() 130 | 131 | def min_var_w_ret(self, ret): 132 | V = self.cov() 133 | suml = [self.returns()[symbol].sum() for symbol in self.asset.keys()] 134 | e = matrix(suml).T 135 | iV = matrix(inv(V)) 136 | num = iV * e * ret 137 | denom = e.T * iV * e 138 | return Series(array(num / denom).flatten(), index=self.asset.keys()) 139 | 140 | def ret_for_w(self, w): 141 | tmp = self.returns() 142 | tmpl = [v * wi for v, wi in zip(tmp.values(), w) ] 143 | return Series(tmpl, index=tmp.keys()).sum() 144 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\QuantPy.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\QuantPy.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/QuantPy.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/QuantPy.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/QuantPy" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/QuantPy" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /quantpy/technical_indicators.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | 4 | 5 | def sma(data, window): 6 | """Simple Moving Average: The average price of a security over a 7 | specific number of periods. 8 | 9 | 'data' is a pandas Series or DataFrame of prices. A ValueError is raised 10 | if 'data' is of different data type. 11 | 12 | 'window' is the number of observations. 13 | It must be a positive integer less than or equal to the length of the data. 14 | Otherwise a ValueError will be raised. 15 | """ 16 | 17 | # todo: maybe add 'long' too? 18 | if not isinstance(window, int) or not 0 < window <= len(data): 19 | raise ValueError("'window' must be an integer " + 20 | "between 1 and %d." % len(data)) 21 | 22 | if not isinstance(data, (pd.Series, pd.DataFrame)): 23 | raise ValueError("'data' must be a pandas Series or DataFrame.") 24 | 25 | return pd.rolling_mean(data, window) 26 | 27 | 28 | def smstd(data, window): 29 | """Simple Moving Standard Deviation: The standard deviation price of a 30 | security over a specific number of periods. 31 | 32 | 'data' is a pandas Series or DataFrame of prices. A ValueError is raised 33 | if 'data' is of different data type. 34 | 35 | 'window' is the number of observations. 36 | It must be a positive integer less than or equal to the length of the data. 37 | Otherwise a ValueError will be raised. 38 | """ 39 | 40 | # todo: maybe add 'long' too? 41 | if not isinstance(window, int) or not 0 < window <= len(data): 42 | raise ValueError("'window' must be an integer " + 43 | "between 1 and %d." % len(data)) 44 | 45 | if not isinstance(data, (pd.Series, pd.DataFrame)): 46 | raise ValueError("'data' must be a pandas Series or DataFrame.") 47 | 48 | return pd.rolling_std(data, window) 49 | 50 | 51 | def ema(data, window): 52 | """Exponential Moving Average: The exponentially weighted average price of 53 | a security over a specific number of periods. 54 | 55 | 'data' is a pandas Series or DataFrame of prices. A ValueError is raised 56 | if 'data' is of different data type. 57 | 58 | 'window' is the number of observations. 59 | It must be a positive integer less than or equal to the length of the data. 60 | Otherwise a ValueError will be raised. 61 | """ 62 | 63 | # todo: maybe add 'long' too? 64 | if not isinstance(window, int) or not 0 < window <= len(data): 65 | raise ValueError("'window' must be an integer " + 66 | "between 1 and %d." % len(data)) 67 | 68 | if not isinstance(data, (pd.Series, pd.DataFrame)): 69 | raise ValueError("'data' must be a pandas Series or DataFrame.") 70 | 71 | return pd.ewma(data, span=window) 72 | 73 | 74 | def emstd(data, window): 75 | """Exponential Moving Standard Deviation: The exponentially weighted 76 | standard deviation of the price of a security over a specific number of 77 | periods. 78 | 79 | 'data' is a pandas Series or DataFrame of prices. A ValueError is raised 80 | if 'data' is of different data type. 81 | 82 | 'window' is the number of observations. 83 | It must be a positive integer less than or equal to the length of the data. 84 | Otherwise a ValueError will be raised. 85 | """ 86 | 87 | # todo: maybe add 'long' too? 88 | if not isinstance(window, int) or not 0 < window <= len(data): 89 | raise ValueError("'window' must be an integer " + 90 | "between 1 and %d." % len(data)) 91 | 92 | if not isinstance(data, (pd.Series, pd.DataFrame)): 93 | raise ValueError("'data' must be a pandas Series or DataFrame.") 94 | 95 | return pd.ewmstd(data, span = window) 96 | 97 | 98 | def macd(data, ema_fast=12, ema_slow=26, ema_macd=9): 99 | """Moving Average Convergence/Divergence. 100 | 101 | Parameters: 102 | 103 | 'data' is a pandas Series or DataFrame of prices. A ValueError is 104 | raised if 'data' is of different data type. 105 | 106 | 'ema_fast' The window period of the "fast" EMA. (Default = 12) 107 | 108 | 'ema_slow' The window period of the "slow" EMA. (Default = 26) 109 | 110 | Returns: 111 | 112 | MACD: The difference between the ema_fast and ema_slow day EMAs of a 113 | security. 114 | 115 | MACD Signal: The ema_macd day EMA of the MACD. 116 | 117 | MACD Histogram: Difference between the MACD and MACD Signal. 118 | """ 119 | 120 | # todo: maybe add 'long' too? 121 | if not isinstance(ema_fast, int) or not 0 < ema_fast <= len(data): 122 | raise ValueError("'ema_fast' must be an integer " + 123 | "between 1 and %d." % len(data)) 124 | 125 | if not isinstance(ema_slow, int) or not 0 < ema_slow <= len(data): 126 | raise ValueError("'ema_slow' must be an integer " + 127 | "between 1 and %d." % len(data)) 128 | 129 | if not isinstance(data, (pd.Series, pd.DataFrame)): 130 | raise ValueError("'data' must be a pandas Series or DataFrame.") 131 | 132 | if not isinstance(ema_macd, int) or not 0 < ema_macd <= len(data): 133 | raise ValueError("'ema_macd' must be an integer " + 134 | "between 1 and %d." % len(data)) 135 | 136 | macd = pd.ewma(data, span = ema_fast) - pd.ewma(data, span = ema_slow) 137 | macds = pd.ewma(macd, span = ema_macd) 138 | 139 | return macd, macds, macd - macds 140 | 141 | def full_stochastic(data, lookback = 14, d_sma = 3, full_sma = 3): 142 | """Full Stochastic Oscillator: The full stochastic oscillator for 143 | (lookback,d_sma,full_sma) is defined by: 144 | 145 | %K = (Current Close - Lowest Low)/(Highest High - Lowest Low) * 100 146 | %D = d_sma-day SMA of %K 147 | full %K = %D 148 | full %D = full_sma-day SMA of full %K 149 | 150 | Parameters: 151 | 152 | 'data' is a pandas Series or DataFrame of prices. Must contain 153 | 'High', 'Low' and 'Close' information. 154 | 155 | 'lookback' Lookback period for calculating highest and Lowst low 156 | 157 | 'd_sma' The SMA used for calculating %D 158 | 159 | 'full_sma' The SMA used for calculating full %D 160 | 161 | Returns: 162 | 163 | full %K: (Or standard %D) as defined above 164 | 165 | full %D: as defined above 166 | 167 | %K: Standard %K as defined above 168 | """ 169 | 170 | l_low = pd.rolling_min(data['Low'], lookback) 171 | h_high = pd.rolling_max(data['High'], lookback) 172 | 173 | K = (data['Close'] - l_low)/(h_high - l_low) * 100.0 174 | full_K = pd.rolling_mean(K,d_sma) 175 | full_D = pd.rolling_mean(full_K,full_sma) 176 | 177 | return full_K, full_D, K 178 | 179 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # QunatPy documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Mar 7 13:05:29 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | #sys.path.insert(0, os.path.abspath('.')) 21 | sys.path.append(os.path.abspath('sphinxext')) 22 | 23 | # -- General configuration ----------------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be extensions 29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = [ 31 | 'matplotlib.sphinxext.mathmpl', 32 | 'matplotlib.sphinxext.only_directives', 33 | 'matplotlib.sphinxext.plot_directive', 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.doctest', 36 | 'ipython_console_highlighting', 37 | 'inheritance_diagram', 38 | 'numpydoc'] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix of source filenames. 44 | source_suffix = '.rst' 45 | 46 | # The encoding of source files. 47 | #source_encoding = 'utf-8-sig' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = u'QunatPy' 54 | copyright = u'2013, Joseph Smidt' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = 'v1.0' 62 | # The full version, including alpha/beta/rc tags. 63 | release = 'v1.0' 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | #language = None 68 | 69 | # There are two options for replacing |today|: either, you set today to some 70 | # non-false value, then it is used: 71 | #today = '' 72 | # Else, today_fmt is used as the format for a strftime call. 73 | #today_fmt = '%B %d, %Y' 74 | 75 | # List of patterns, relative to source directory, that match files and 76 | # directories to ignore when looking for source files. 77 | exclude_patterns = ['_build'] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all documents. 80 | #default_role = None 81 | 82 | # If true, '()' will be appended to :func: etc. cross-reference text. 83 | #add_function_parentheses = True 84 | 85 | # If true, the current module name will be prepended to all description 86 | # unit titles (such as .. function::). 87 | #add_module_names = True 88 | 89 | # If true, sectionauthor and moduleauthor directives will be shown in the 90 | # output. They are ignored by default. 91 | #show_authors = False 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = 'sphinx' 95 | 96 | # A list of ignored prefixes for module index sorting. 97 | #modindex_common_prefix = [] 98 | 99 | 100 | # -- Options for HTML output --------------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. See the documentation for 103 | # a list of builtin themes. 104 | html_theme = 'default' 105 | 106 | # Theme options are theme-specific and customize the look and feel of a theme 107 | # further. For a list of options available for each theme, see the 108 | # documentation. 109 | #html_theme_options = {} 110 | 111 | # Add any paths that contain custom themes here, relative to this directory. 112 | #html_theme_path = [] 113 | 114 | # The name for this set of Sphinx documents. If None, it defaults to 115 | # " v documentation". 116 | #html_title = None 117 | 118 | # A shorter title for the navigation bar. Default is the same as html_title. 119 | #html_short_title = None 120 | 121 | # The name of an image file (relative to this directory) to place at the top 122 | # of the sidebar. 123 | #html_logo = None 124 | 125 | # The name of an image file (within the static path) to use as favicon of the 126 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 127 | # pixels large. 128 | #html_favicon = None 129 | 130 | # Add any paths that contain custom static files (such as style sheets) here, 131 | # relative to this directory. They are copied after the builtin static files, 132 | # so a file named "default.css" will overwrite the builtin "default.css". 133 | html_static_path = ['_static'] 134 | 135 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 136 | # using the given strftime format. 137 | #html_last_updated_fmt = '%b %d, %Y' 138 | 139 | # If true, SmartyPants will be used to convert quotes and dashes to 140 | # typographically correct entities. 141 | #html_use_smartypants = True 142 | 143 | # Custom sidebar templates, maps document names to template names. 144 | #html_sidebars = {} 145 | 146 | # Additional templates that should be rendered to pages, maps page names to 147 | # template names. 148 | #html_additional_pages = {} 149 | 150 | # If false, no module index is generated. 151 | #html_domain_indices = True 152 | 153 | # If false, no index is generated. 154 | #html_use_index = True 155 | 156 | # If true, the index is split into individual pages for each letter. 157 | #html_split_index = False 158 | 159 | # If true, links to the reST sources are added to the pages. 160 | #html_show_sourcelink = True 161 | 162 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 163 | #html_show_sphinx = True 164 | 165 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 166 | #html_show_copyright = True 167 | 168 | # If true, an OpenSearch description file will be output, and all pages will 169 | # contain a tag referring to it. The value of this option must be the 170 | # base URL from which the finished HTML is served. 171 | #html_use_opensearch = '' 172 | 173 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 174 | #html_file_suffix = None 175 | 176 | # Output file base name for HTML help builder. 177 | htmlhelp_basename = 'QunatPydoc' 178 | 179 | 180 | # -- Options for LaTeX output -------------------------------------------------- 181 | 182 | latex_elements = { 183 | # The paper size ('letterpaper' or 'a4paper'). 184 | #'papersize': 'letterpaper', 185 | 186 | # The font size ('10pt', '11pt' or '12pt'). 187 | #'pointsize': '10pt', 188 | 189 | # Additional stuff for the LaTeX preamble. 190 | #'preamble': '', 191 | } 192 | 193 | # Grouping the document tree into LaTeX files. List of tuples 194 | # (source start file, target name, title, author, documentclass [howto/manual]). 195 | latex_documents = [ 196 | ('index', 'QunatPy.tex', u'QunatPy Documentation', 197 | u'Joseph Smidt', 'manual'), 198 | ] 199 | 200 | # The name of an image file (relative to this directory) to place at the top of 201 | # the title page. 202 | #latex_logo = None 203 | 204 | # For "manual" documents, if this is true, then toplevel headings are parts, 205 | # not chapters. 206 | #latex_use_parts = False 207 | 208 | # If true, show page references after internal links. 209 | #latex_show_pagerefs = False 210 | 211 | # If true, show URL addresses after external links. 212 | #latex_show_urls = False 213 | 214 | # Documents to append as an appendix to all manuals. 215 | #latex_appendices = [] 216 | 217 | # If false, no module index is generated. 218 | #latex_domain_indices = True 219 | 220 | 221 | # -- Options for manual page output -------------------------------------------- 222 | 223 | # One entry per manual page. List of tuples 224 | # (source start file, name, description, authors, manual section). 225 | man_pages = [ 226 | ('index', 'qunatpy', u'QunatPy Documentation', 227 | [u'Joseph Smidt'], 1) 228 | ] 229 | 230 | # If true, show URL addresses after external links. 231 | #man_show_urls = False 232 | 233 | 234 | # -- Options for Texinfo output ------------------------------------------------ 235 | 236 | # Grouping the document tree into Texinfo files. List of tuples 237 | # (source start file, target name, title, author, 238 | # dir menu entry, description, category) 239 | texinfo_documents = [ 240 | ('index', 'QunatPy', u'QunatPy Documentation', 241 | u'Joseph Smidt', 'QunatPy', 'One line description of project.', 242 | 'Miscellaneous'), 243 | ] 244 | 245 | # Documents to append as an appendix to all manuals. 246 | #texinfo_appendices = [] 247 | 248 | # If false, no module index is generated. 249 | #texinfo_domain_indices = True 250 | 251 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 252 | #texinfo_show_urls = 'footnote' 253 | -------------------------------------------------------------------------------- /docs/_static/default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Alternate Sphinx design 3 | * Originally created by Armin Ronacher for Werkzeug, adapted by Georg Brandl. 4 | */ 5 | 6 | body { 7 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif; 8 | font-size: 14px; 9 | letter-spacing: -0.01em; 10 | line-height: 150%; 11 | text-align: center; 12 | /*background-color: #AFC1C4; */ 13 | background-color: #BFD1D4; 14 | color: black; 15 | padding: 0; 16 | border: 1px solid #aaa; 17 | 18 | margin: 0px 80px 0px 80px; 19 | min-width: 740px; 20 | } 21 | 22 | a { 23 | color: #CA7900; 24 | text-decoration: none; 25 | } 26 | 27 | a:hover { 28 | color: #2491CF; 29 | } 30 | 31 | pre { 32 | font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 33 | font-size: 0.95em; 34 | letter-spacing: 0.015em; 35 | padding: 0.5em; 36 | border: 1px solid #ccc; 37 | background-color: #f8f8f8; 38 | } 39 | 40 | td.linenos pre { 41 | padding: 0.5em 0; 42 | border: 0; 43 | background-color: transparent; 44 | color: #aaa; 45 | } 46 | 47 | table.highlighttable { 48 | margin-left: 0.5em; 49 | } 50 | 51 | table.highlighttable td { 52 | padding: 0 0.5em 0 0.5em; 53 | } 54 | 55 | cite, code, tt { 56 | font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 57 | font-size: 0.95em; 58 | letter-spacing: 0.01em; 59 | } 60 | 61 | hr { 62 | border: 1px solid #abc; 63 | margin: 2em; 64 | } 65 | 66 | tt { 67 | background-color: #f2f2f2; 68 | border-bottom: 1px solid #ddd; 69 | color: #333; 70 | } 71 | 72 | tt.descname { 73 | background-color: transparent; 74 | font-weight: bold; 75 | font-size: 1.2em; 76 | border: 0; 77 | } 78 | 79 | tt.descclassname { 80 | background-color: transparent; 81 | border: 0; 82 | } 83 | 84 | tt.xref { 85 | background-color: transparent; 86 | font-weight: bold; 87 | border: 0; 88 | } 89 | 90 | a tt { 91 | background-color: transparent; 92 | font-weight: bold; 93 | border: 0; 94 | color: #CA7900; 95 | } 96 | 97 | a tt:hover { 98 | color: #2491CF; 99 | } 100 | 101 | dl { 102 | margin-bottom: 15px; 103 | } 104 | 105 | dd p { 106 | margin-top: 0px; 107 | } 108 | 109 | dd ul, dd table { 110 | margin-bottom: 10px; 111 | } 112 | 113 | dd { 114 | margin-top: 3px; 115 | margin-bottom: 10px; 116 | margin-left: 30px; 117 | } 118 | 119 | .refcount { 120 | color: #060; 121 | } 122 | 123 | dt:target, 124 | .highlight { 125 | background-color: #fbe54e; 126 | } 127 | 128 | dl.class, dl.function { 129 | border-top: 2px solid #888; 130 | } 131 | 132 | dl.method, dl.attribute { 133 | border-top: 1px solid #aaa; 134 | } 135 | 136 | dl.glossary dt { 137 | font-weight: bold; 138 | font-size: 1.1em; 139 | } 140 | 141 | pre { 142 | line-height: 120%; 143 | } 144 | 145 | pre a { 146 | color: inherit; 147 | text-decoration: underline; 148 | } 149 | 150 | .first { 151 | margin-top: 0 !important; 152 | } 153 | 154 | div.document { 155 | background-color: white; 156 | text-align: left; 157 | background-image: url(contents.png); 158 | background-repeat: repeat-x; 159 | } 160 | 161 | /* 162 | div.documentwrapper { 163 | width: 100%; 164 | } 165 | */ 166 | 167 | div.clearer { 168 | clear: both; 169 | } 170 | 171 | div.related h3 { 172 | display: none; 173 | } 174 | 175 | div.related ul { 176 | background-image: url(navigation.png); 177 | height: 2em; 178 | list-style: none; 179 | border-top: 1px solid #ddd; 180 | border-bottom: 1px solid #ddd; 181 | margin: 0; 182 | padding-left: 10px; 183 | } 184 | 185 | div.related ul li { 186 | margin: 0; 187 | padding: 0; 188 | height: 2em; 189 | float: left; 190 | } 191 | 192 | div.related ul li.right { 193 | float: right; 194 | margin-right: 5px; 195 | } 196 | 197 | div.related ul li a { 198 | margin: 0; 199 | padding: 0 5px 0 5px; 200 | line-height: 1.75em; 201 | color: #EE9816; 202 | } 203 | 204 | div.related ul li a:hover { 205 | color: #3CA8E7; 206 | } 207 | 208 | div.body { 209 | margin: 0; 210 | padding: 0.5em 20px 20px 20px; 211 | } 212 | 213 | div.bodywrapper { 214 | margin: 0 240px 0 0; 215 | border-right: 1px solid #ccc; 216 | } 217 | 218 | div.body a { 219 | text-decoration: underline; 220 | } 221 | 222 | div.sphinxsidebar { 223 | margin: 0; 224 | padding: 0.5em 15px 15px 0; 225 | width: 210px; 226 | float: right; 227 | text-align: left; 228 | /* margin-left: -100%; */ 229 | } 230 | 231 | div.sphinxsidebar h4, div.sphinxsidebar h3 { 232 | margin: 1em 0 0.5em 0; 233 | font-size: 0.9em; 234 | padding: 0.1em 0 0.1em 0.5em; 235 | color: white; 236 | border: 1px solid #86989B; 237 | background-color: #AFC1C4; 238 | } 239 | 240 | div.sphinxsidebar ul { 241 | padding-left: 1.5em; 242 | margin-top: 7px; 243 | list-style: none; 244 | padding: 0; 245 | line-height: 130%; 246 | } 247 | 248 | div.sphinxsidebar ul ul { 249 | list-style: square; 250 | margin-left: 20px; 251 | } 252 | 253 | p { 254 | margin: 0.8em 0 0.5em 0; 255 | } 256 | 257 | p.rubric { 258 | font-weight: bold; 259 | } 260 | 261 | h1 { 262 | margin: 0; 263 | padding: 0.7em 0 0.3em 0; 264 | font-size: 1.5em; 265 | color: #11557C; 266 | } 267 | 268 | h2 { 269 | margin: 1.3em 0 0.2em 0; 270 | font-size: 1.35em; 271 | padding: 0; 272 | } 273 | 274 | h3 { 275 | margin: 1em 0 -0.3em 0; 276 | font-size: 1.2em; 277 | } 278 | 279 | h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { 280 | color: black!important; 281 | } 282 | 283 | h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor { 284 | display: none; 285 | margin: 0 0 0 0.3em; 286 | padding: 0 0.2em 0 0.2em; 287 | color: #aaa!important; 288 | } 289 | 290 | h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, 291 | h5:hover a.anchor, h6:hover a.anchor { 292 | display: inline; 293 | } 294 | 295 | h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, 296 | h5 a.anchor:hover, h6 a.anchor:hover { 297 | color: #777; 298 | background-color: #eee; 299 | } 300 | 301 | table { 302 | border-collapse: collapse; 303 | margin: 0 -0.5em 0 -0.5em; 304 | } 305 | 306 | table td, table th { 307 | padding: 0.2em 0.5em 0.2em 0.5em; 308 | } 309 | 310 | div.footer { 311 | background-color: #E3EFF1; 312 | color: #86989B; 313 | padding: 3px 8px 3px 0; 314 | clear: both; 315 | font-size: 0.8em; 316 | text-align: right; 317 | } 318 | 319 | div.footer a { 320 | color: #86989B; 321 | text-decoration: underline; 322 | } 323 | 324 | div.pagination { 325 | margin-top: 2em; 326 | padding-top: 0.5em; 327 | border-top: 1px solid black; 328 | text-align: center; 329 | } 330 | 331 | div.sphinxsidebar ul.toc { 332 | margin: 1em 0 1em 0; 333 | padding: 0 0 0 0.5em; 334 | list-style: none; 335 | } 336 | 337 | div.sphinxsidebar ul.toc li { 338 | margin: 0.5em 0 0.5em 0; 339 | font-size: 0.9em; 340 | line-height: 130%; 341 | } 342 | 343 | div.sphinxsidebar ul.toc li p { 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | div.sphinxsidebar ul.toc ul { 349 | margin: 0.2em 0 0.2em 0; 350 | padding: 0 0 0 1.8em; 351 | } 352 | 353 | div.sphinxsidebar ul.toc ul li { 354 | padding: 0; 355 | } 356 | 357 | div.admonition, div.warning { 358 | font-size: 0.9em; 359 | margin: 1em 0 0 0; 360 | border: 1px solid #86989B; 361 | background-color: #f7f7f7; 362 | } 363 | 364 | div.admonition p, div.warning p { 365 | margin: 0.5em 1em 0.5em 1em; 366 | padding: 0; 367 | } 368 | 369 | div.admonition pre, div.warning pre { 370 | margin: 0.4em 1em 0.4em 1em; 371 | } 372 | 373 | div.admonition p.admonition-title, 374 | div.warning p.admonition-title { 375 | margin: 0; 376 | padding: 0.1em 0 0.1em 0.5em; 377 | color: white; 378 | border-bottom: 1px solid #86989B; 379 | font-weight: bold; 380 | background-color: #AFC1C4; 381 | } 382 | 383 | div.warning { 384 | border: 1px solid #940000; 385 | } 386 | 387 | div.warning p.admonition-title { 388 | background-color: #CF0000; 389 | border-bottom-color: #940000; 390 | } 391 | 392 | div.admonition ul, div.admonition ol, 393 | div.warning ul, div.warning ol { 394 | margin: 0.1em 0.5em 0.5em 3em; 395 | padding: 0; 396 | } 397 | 398 | div.versioninfo { 399 | margin: 1em 0 0 0; 400 | border: 1px solid #ccc; 401 | background-color: #DDEAF0; 402 | padding: 8px; 403 | line-height: 1.3em; 404 | font-size: 0.9em; 405 | } 406 | 407 | 408 | a.headerlink { 409 | color: #c60f0f!important; 410 | font-size: 1em; 411 | margin-left: 6px; 412 | padding: 0 4px 0 4px; 413 | text-decoration: none!important; 414 | visibility: hidden; 415 | } 416 | 417 | h1:hover > a.headerlink, 418 | h2:hover > a.headerlink, 419 | h3:hover > a.headerlink, 420 | h4:hover > a.headerlink, 421 | h5:hover > a.headerlink, 422 | h6:hover > a.headerlink, 423 | dt:hover > a.headerlink { 424 | visibility: visible; 425 | } 426 | 427 | a.headerlink:hover { 428 | background-color: #ccc; 429 | color: white!important; 430 | } 431 | 432 | table.indextable td { 433 | text-align: left; 434 | vertical-align: top; 435 | } 436 | 437 | table.indextable dl, table.indextable dd { 438 | margin-top: 0; 439 | margin-bottom: 0; 440 | } 441 | 442 | table.indextable tr.pcap { 443 | height: 10px; 444 | } 445 | 446 | table.indextable tr.cap { 447 | margin-top: 10px; 448 | background-color: #f2f2f2; 449 | } 450 | 451 | img.toggler { 452 | margin-right: 3px; 453 | margin-top: 3px; 454 | cursor: pointer; 455 | } 456 | 457 | img.inheritance { 458 | border: 0px 459 | } 460 | 461 | form.pfform { 462 | margin: 10px 0 20px 0; 463 | } 464 | 465 | table.contentstable { 466 | width: 90%; 467 | } 468 | 469 | table.contentstable p.biglink { 470 | line-height: 150%; 471 | } 472 | 473 | a.biglink { 474 | font-size: 1.3em; 475 | } 476 | 477 | span.linkdescr { 478 | font-style: italic; 479 | padding-top: 5px; 480 | font-size: 90%; 481 | } 482 | 483 | ul.search { 484 | margin: 10px 0 0 20px; 485 | padding: 0; 486 | } 487 | 488 | ul.search li { 489 | padding: 5px 0 5px 20px; 490 | background-image: url(file.png); 491 | background-repeat: no-repeat; 492 | background-position: 0 7px; 493 | } 494 | 495 | ul.search li a { 496 | font-weight: bold; 497 | } 498 | 499 | ul.search li div.context { 500 | color: #888; 501 | margin: 2px 0 0 30px; 502 | text-align: left; 503 | } 504 | 505 | ul.keywordmatches li.goodmatch a { 506 | font-weight: bold; 507 | } 508 | -------------------------------------------------------------------------------- /docs/sphinxext/inheritance_diagram.py: -------------------------------------------------------------------------------- 1 | """ 2 | Defines a docutils directive for inserting inheritance diagrams. 3 | 4 | Provide the directive with one or more classes or modules (separated 5 | by whitespace). For modules, all of the classes in that module will 6 | be used. 7 | 8 | Example:: 9 | 10 | Given the following classes: 11 | 12 | class A: pass 13 | class B(A): pass 14 | class C(A): pass 15 | class D(B, C): pass 16 | class E(B): pass 17 | 18 | .. inheritance-diagram: D E 19 | 20 | Produces a graph like the following: 21 | 22 | A 23 | / \ 24 | B C 25 | / \ / 26 | E D 27 | 28 | The graph is inserted as a PNG+image map into HTML and a PDF in 29 | LaTeX. 30 | """ 31 | 32 | import inspect 33 | import os 34 | import re 35 | import subprocess 36 | try: 37 | from hashlib import md5 38 | except ImportError: 39 | from md5 import md5 40 | 41 | from docutils.nodes import Body, Element 42 | from docutils.parsers.rst import directives 43 | from sphinx.roles import xfileref_role 44 | 45 | def my_import(name): 46 | """Module importer - taken from the python documentation. 47 | 48 | This function allows importing names with dots in them.""" 49 | 50 | mod = __import__(name) 51 | components = name.split('.') 52 | for comp in components[1:]: 53 | mod = getattr(mod, comp) 54 | return mod 55 | 56 | class DotException(Exception): 57 | pass 58 | 59 | class InheritanceGraph(object): 60 | """ 61 | Given a list of classes, determines the set of classes that 62 | they inherit from all the way to the root "object", and then 63 | is able to generate a graphviz dot graph from them. 64 | """ 65 | def __init__(self, class_names, show_builtins=False): 66 | """ 67 | *class_names* is a list of child classes to show bases from. 68 | 69 | If *show_builtins* is True, then Python builtins will be shown 70 | in the graph. 71 | """ 72 | self.class_names = class_names 73 | self.classes = self._import_classes(class_names) 74 | self.all_classes = self._all_classes(self.classes) 75 | if len(self.all_classes) == 0: 76 | raise ValueError("No classes found for inheritance diagram") 77 | self.show_builtins = show_builtins 78 | 79 | py_sig_re = re.compile(r'''^([\w.]*\.)? # class names 80 | (\w+) \s* $ # optionally arguments 81 | ''', re.VERBOSE) 82 | 83 | def _import_class_or_module(self, name): 84 | """ 85 | Import a class using its fully-qualified *name*. 86 | """ 87 | try: 88 | path, base = self.py_sig_re.match(name).groups() 89 | except: 90 | raise ValueError( 91 | "Invalid class or module '%s' specified for inheritance diagram" % name) 92 | fullname = (path or '') + base 93 | path = (path and path.rstrip('.')) 94 | if not path: 95 | path = base 96 | try: 97 | module = __import__(path, None, None, []) 98 | # We must do an import of the fully qualified name. Otherwise if a 99 | # subpackage 'a.b' is requested where 'import a' does NOT provide 100 | # 'a.b' automatically, then 'a.b' will not be found below. This 101 | # second call will force the equivalent of 'import a.b' to happen 102 | # after the top-level import above. 103 | my_import(fullname) 104 | 105 | except ImportError: 106 | raise ValueError( 107 | "Could not import class or module '%s' specified for inheritance diagram" % name) 108 | 109 | try: 110 | todoc = module 111 | for comp in fullname.split('.')[1:]: 112 | todoc = getattr(todoc, comp) 113 | except AttributeError: 114 | raise ValueError( 115 | "Could not find class or module '%s' specified for inheritance diagram" % name) 116 | 117 | # If a class, just return it 118 | if inspect.isclass(todoc): 119 | return [todoc] 120 | elif inspect.ismodule(todoc): 121 | classes = [] 122 | for cls in todoc.__dict__.values(): 123 | if inspect.isclass(cls) and cls.__module__ == todoc.__name__: 124 | classes.append(cls) 125 | return classes 126 | raise ValueError( 127 | "'%s' does not resolve to a class or module" % name) 128 | 129 | def _import_classes(self, class_names): 130 | """ 131 | Import a list of classes. 132 | """ 133 | classes = [] 134 | for name in class_names: 135 | classes.extend(self._import_class_or_module(name)) 136 | return classes 137 | 138 | def _all_classes(self, classes): 139 | """ 140 | Return a list of all classes that are ancestors of *classes*. 141 | """ 142 | all_classes = {} 143 | 144 | def recurse(cls): 145 | all_classes[cls] = None 146 | for c in cls.__bases__: 147 | if c not in all_classes: 148 | recurse(c) 149 | 150 | for cls in classes: 151 | recurse(cls) 152 | 153 | return all_classes.keys() 154 | 155 | def class_name(self, cls, parts=0): 156 | """ 157 | Given a class object, return a fully-qualified name. This 158 | works for things I've tested in matplotlib so far, but may not 159 | be completely general. 160 | """ 161 | module = cls.__module__ 162 | if module == '__builtin__': 163 | fullname = cls.__name__ 164 | else: 165 | fullname = "%s.%s" % (module, cls.__name__) 166 | if parts == 0: 167 | return fullname 168 | name_parts = fullname.split('.') 169 | return '.'.join(name_parts[-parts:]) 170 | 171 | def get_all_class_names(self): 172 | """ 173 | Get all of the class names involved in the graph. 174 | """ 175 | return [self.class_name(x) for x in self.all_classes] 176 | 177 | # These are the default options for graphviz 178 | default_graph_options = { 179 | "rankdir": "LR", 180 | "size": '"8.0, 12.0"' 181 | } 182 | default_node_options = { 183 | "shape": "box", 184 | "fontsize": 10, 185 | "height": 0.25, 186 | "fontname": "Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans", 187 | "style": '"setlinewidth(0.5)"' 188 | } 189 | default_edge_options = { 190 | "arrowsize": 0.5, 191 | "style": '"setlinewidth(0.5)"' 192 | } 193 | 194 | def _format_node_options(self, options): 195 | return ','.join(["%s=%s" % x for x in options.items()]) 196 | def _format_graph_options(self, options): 197 | return ''.join(["%s=%s;\n" % x for x in options.items()]) 198 | 199 | def generate_dot(self, fd, name, parts=0, urls={}, 200 | graph_options={}, node_options={}, 201 | edge_options={}): 202 | """ 203 | Generate a graphviz dot graph from the classes that 204 | were passed in to __init__. 205 | 206 | *fd* is a Python file-like object to write to. 207 | 208 | *name* is the name of the graph 209 | 210 | *urls* is a dictionary mapping class names to http urls 211 | 212 | *graph_options*, *node_options*, *edge_options* are 213 | dictionaries containing key/value pairs to pass on as graphviz 214 | properties. 215 | """ 216 | g_options = self.default_graph_options.copy() 217 | g_options.update(graph_options) 218 | n_options = self.default_node_options.copy() 219 | n_options.update(node_options) 220 | e_options = self.default_edge_options.copy() 221 | e_options.update(edge_options) 222 | 223 | fd.write('digraph %s {\n' % name) 224 | fd.write(self._format_graph_options(g_options)) 225 | 226 | for cls in self.all_classes: 227 | if not self.show_builtins and cls in __builtins__.values(): 228 | continue 229 | 230 | name = self.class_name(cls, parts) 231 | 232 | # Write the node 233 | this_node_options = n_options.copy() 234 | url = urls.get(self.class_name(cls)) 235 | if url is not None: 236 | this_node_options['URL'] = '"%s"' % url 237 | fd.write(' "%s" [%s];\n' % 238 | (name, self._format_node_options(this_node_options))) 239 | 240 | # Write the edges 241 | for base in cls.__bases__: 242 | if not self.show_builtins and base in __builtins__.values(): 243 | continue 244 | 245 | base_name = self.class_name(base, parts) 246 | fd.write(' "%s" -> "%s" [%s];\n' % 247 | (base_name, name, 248 | self._format_node_options(e_options))) 249 | fd.write('}\n') 250 | 251 | def run_dot(self, args, name, parts=0, urls={}, 252 | graph_options={}, node_options={}, edge_options={}): 253 | """ 254 | Run graphviz 'dot' over this graph, returning whatever 'dot' 255 | writes to stdout. 256 | 257 | *args* will be passed along as commandline arguments. 258 | 259 | *name* is the name of the graph 260 | 261 | *urls* is a dictionary mapping class names to http urls 262 | 263 | Raises DotException for any of the many os and 264 | installation-related errors that may occur. 265 | """ 266 | try: 267 | dot = subprocess.Popen(['dot'] + list(args), 268 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, 269 | close_fds=True) 270 | except OSError: 271 | raise DotException("Could not execute 'dot'. Are you sure you have 'graphviz' installed?") 272 | except ValueError: 273 | raise DotException("'dot' called with invalid arguments") 274 | except: 275 | raise DotException("Unexpected error calling 'dot'") 276 | 277 | self.generate_dot(dot.stdin, name, parts, urls, graph_options, 278 | node_options, edge_options) 279 | dot.stdin.close() 280 | result = dot.stdout.read() 281 | returncode = dot.wait() 282 | if returncode != 0: 283 | raise DotException("'dot' returned the errorcode %d" % returncode) 284 | return result 285 | 286 | class inheritance_diagram(Body, Element): 287 | """ 288 | A docutils node to use as a placeholder for the inheritance 289 | diagram. 290 | """ 291 | pass 292 | 293 | def inheritance_diagram_directive(name, arguments, options, content, lineno, 294 | content_offset, block_text, state, 295 | state_machine): 296 | """ 297 | Run when the inheritance_diagram directive is first encountered. 298 | """ 299 | node = inheritance_diagram() 300 | 301 | class_names = arguments 302 | 303 | # Create a graph starting with the list of classes 304 | graph = InheritanceGraph(class_names) 305 | 306 | # Create xref nodes for each target of the graph's image map and 307 | # add them to the doc tree so that Sphinx can resolve the 308 | # references to real URLs later. These nodes will eventually be 309 | # removed from the doctree after we're done with them. 310 | for name in graph.get_all_class_names(): 311 | refnodes, x = xfileref_role( 312 | 'class', ':class:`%s`' % name, name, 0, state) 313 | node.extend(refnodes) 314 | # Store the graph object so we can use it to generate the 315 | # dot file later 316 | node['graph'] = graph 317 | # Store the original content for use as a hash 318 | node['parts'] = options.get('parts', 0) 319 | node['content'] = " ".join(class_names) 320 | return [node] 321 | 322 | def get_graph_hash(node): 323 | return md5(node['content'] + str(node['parts'])).hexdigest()[-10:] 324 | 325 | def html_output_graph(self, node): 326 | """ 327 | Output the graph for HTML. This will insert a PNG with clickable 328 | image map. 329 | """ 330 | graph = node['graph'] 331 | parts = node['parts'] 332 | 333 | graph_hash = get_graph_hash(node) 334 | name = "inheritance%s" % graph_hash 335 | path = '_images' 336 | dest_path = os.path.join(setup.app.builder.outdir, path) 337 | if not os.path.exists(dest_path): 338 | os.makedirs(dest_path) 339 | png_path = os.path.join(dest_path, name + ".png") 340 | path = setup.app.builder.imgpath 341 | 342 | # Create a mapping from fully-qualified class names to URLs. 343 | urls = {} 344 | for child in node: 345 | if child.get('refuri') is not None: 346 | urls[child['reftitle']] = child.get('refuri') 347 | elif child.get('refid') is not None: 348 | urls[child['reftitle']] = '#' + child.get('refid') 349 | 350 | # These arguments to dot will save a PNG file to disk and write 351 | # an HTML image map to stdout. 352 | image_map = graph.run_dot(['-Tpng', '-o%s' % png_path, '-Tcmapx'], 353 | name, parts, urls) 354 | return ('%s' % 355 | (path, name, name, image_map)) 356 | 357 | def latex_output_graph(self, node): 358 | """ 359 | Output the graph for LaTeX. This will insert a PDF. 360 | """ 361 | graph = node['graph'] 362 | parts = node['parts'] 363 | 364 | graph_hash = get_graph_hash(node) 365 | name = "inheritance%s" % graph_hash 366 | dest_path = os.path.abspath(os.path.join(setup.app.builder.outdir, '_images')) 367 | if not os.path.exists(dest_path): 368 | os.makedirs(dest_path) 369 | pdf_path = os.path.abspath(os.path.join(dest_path, name + ".pdf")) 370 | 371 | graph.run_dot(['-Tpdf', '-o%s' % pdf_path], 372 | name, parts, graph_options={'size': '"6.0,6.0"'}) 373 | return '\n\\includegraphics{%s}\n\n' % pdf_path 374 | 375 | def visit_inheritance_diagram(inner_func): 376 | """ 377 | This is just a wrapper around html/latex_output_graph to make it 378 | easier to handle errors and insert warnings. 379 | """ 380 | def visitor(self, node): 381 | try: 382 | content = inner_func(self, node) 383 | except DotException, e: 384 | # Insert the exception as a warning in the document 385 | warning = self.document.reporter.warning(str(e), line=node.line) 386 | warning.parent = node 387 | node.children = [warning] 388 | else: 389 | source = self.document.attributes['source'] 390 | self.body.append(content) 391 | node.children = [] 392 | return visitor 393 | 394 | def do_nothing(self, node): 395 | pass 396 | 397 | def setup(app): 398 | setup.app = app 399 | setup.confdir = app.confdir 400 | 401 | app.add_node( 402 | inheritance_diagram, 403 | latex=(visit_inheritance_diagram(latex_output_graph), do_nothing), 404 | html=(visit_inheritance_diagram(html_output_graph), do_nothing)) 405 | app.add_directive( 406 | 'inheritance-diagram', inheritance_diagram_directive, 407 | False, (1, 100, 0), parts = directives.nonnegative_int) 408 | -------------------------------------------------------------------------------- /docs/sphinxext/docscrape.py: -------------------------------------------------------------------------------- 1 | """Extract reference documentation from the NumPy source tree. 2 | 3 | """ 4 | 5 | import inspect 6 | import textwrap 7 | import re 8 | import pydoc 9 | from StringIO import StringIO 10 | from warnings import warn 11 | 4 12 | class Reader(object): 13 | """A line-based string reader. 14 | 15 | """ 16 | def __init__(self, data): 17 | """ 18 | Parameters 19 | ---------- 20 | data : str 21 | String with lines separated by '\n'. 22 | 23 | """ 24 | if isinstance(data,list): 25 | self._str = data 26 | else: 27 | self._str = data.split('\n') # store string as list of lines 28 | 29 | self.reset() 30 | 31 | def __getitem__(self, n): 32 | return self._str[n] 33 | 34 | def reset(self): 35 | self._l = 0 # current line nr 36 | 37 | def read(self): 38 | if not self.eof(): 39 | out = self[self._l] 40 | self._l += 1 41 | return out 42 | else: 43 | return '' 44 | 45 | def seek_next_non_empty_line(self): 46 | for l in self[self._l:]: 47 | if l.strip(): 48 | break 49 | else: 50 | self._l += 1 51 | 52 | def eof(self): 53 | return self._l >= len(self._str) 54 | 55 | def read_to_condition(self, condition_func): 56 | start = self._l 57 | for line in self[start:]: 58 | if condition_func(line): 59 | return self[start:self._l] 60 | self._l += 1 61 | if self.eof(): 62 | return self[start:self._l+1] 63 | return [] 64 | 65 | def read_to_next_empty_line(self): 66 | self.seek_next_non_empty_line() 67 | def is_empty(line): 68 | return not line.strip() 69 | return self.read_to_condition(is_empty) 70 | 71 | def read_to_next_unindented_line(self): 72 | def is_unindented(line): 73 | return (line.strip() and (len(line.lstrip()) == len(line))) 74 | return self.read_to_condition(is_unindented) 75 | 76 | def peek(self,n=0): 77 | if self._l + n < len(self._str): 78 | return self[self._l + n] 79 | else: 80 | return '' 81 | 82 | def is_empty(self): 83 | return not ''.join(self._str).strip() 84 | 85 | 86 | class NumpyDocString(object): 87 | def __init__(self,docstring): 88 | docstring = textwrap.dedent(docstring).split('\n') 89 | 90 | self._doc = Reader(docstring) 91 | self._parsed_data = { 92 | 'Signature': '', 93 | 'Summary': [''], 94 | 'Extended Summary': [], 95 | 'Parameters': [], 96 | 'Returns': [], 97 | 'Raises': [], 98 | 'Warns': [], 99 | 'Other Parameters': [], 100 | 'Attributes': [], 101 | 'Methods': [], 102 | 'See Also': [], 103 | 'Notes': [], 104 | 'Warnings': [], 105 | 'References': '', 106 | 'Examples': '', 107 | 'index': {} 108 | } 109 | 110 | self._parse() 111 | 112 | def __getitem__(self,key): 113 | return self._parsed_data[key] 114 | 115 | def __setitem__(self,key,val): 116 | if not self._parsed_data.has_key(key): 117 | warn("Unknown section %s" % key) 118 | else: 119 | self._parsed_data[key] = val 120 | 121 | def _is_at_section(self): 122 | self._doc.seek_next_non_empty_line() 123 | 124 | if self._doc.eof(): 125 | return False 126 | 127 | l1 = self._doc.peek().strip() # e.g. Parameters 128 | 129 | if l1.startswith('.. index::'): 130 | return True 131 | 132 | l2 = self._doc.peek(1).strip() # ---------- or ========== 133 | return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1)) 134 | 135 | def _strip(self,doc): 136 | i = 0 137 | j = 0 138 | for i,line in enumerate(doc): 139 | if line.strip(): break 140 | 141 | for j,line in enumerate(doc[::-1]): 142 | if line.strip(): break 143 | 144 | return doc[i:len(doc)-j] 145 | 146 | def _read_to_next_section(self): 147 | section = self._doc.read_to_next_empty_line() 148 | 149 | while not self._is_at_section() and not self._doc.eof(): 150 | if not self._doc.peek(-1).strip(): # previous line was empty 151 | section += [''] 152 | 153 | section += self._doc.read_to_next_empty_line() 154 | 155 | return section 156 | 157 | def _read_sections(self): 158 | while not self._doc.eof(): 159 | data = self._read_to_next_section() 160 | name = data[0].strip() 161 | 162 | if name.startswith('..'): # index section 163 | yield name, data[1:] 164 | elif len(data) < 2: 165 | yield StopIteration 166 | else: 167 | yield name, self._strip(data[2:]) 168 | 169 | def _parse_param_list(self,content): 170 | r = Reader(content) 171 | params = [] 172 | while not r.eof(): 173 | header = r.read().strip() 174 | if ' : ' in header: 175 | arg_name, arg_type = header.split(' : ')[:2] 176 | else: 177 | arg_name, arg_type = header, '' 178 | 179 | desc = r.read_to_next_unindented_line() 180 | desc = dedent_lines(desc) 181 | 182 | params.append((arg_name,arg_type,desc)) 183 | 184 | return params 185 | 186 | 187 | _name_rgx = re.compile(r"^\s*(:(?P\w+):`(?P[a-zA-Z0-9_.-]+)`|" 188 | r" (?P[a-zA-Z0-9_.-]+))\s*", re.X) 189 | def _parse_see_also(self, content): 190 | """ 191 | func_name : Descriptive text 192 | continued text 193 | another_func_name : Descriptive text 194 | func_name1, func_name2, :meth:`func_name`, func_name3 195 | 196 | """ 197 | items = [] 198 | 199 | def parse_item_name(text): 200 | """Match ':role:`name`' or 'name'""" 201 | m = self._name_rgx.match(text) 202 | if m: 203 | g = m.groups() 204 | if g[1] is None: 205 | return g[3], None 206 | else: 207 | return g[2], g[1] 208 | raise ValueError("%s is not a item name" % text) 209 | 210 | def push_item(name, rest): 211 | if not name: 212 | return 213 | name, role = parse_item_name(name) 214 | items.append((name, list(rest), role)) 215 | del rest[:] 216 | 217 | current_func = None 218 | rest = [] 219 | 220 | for line in content: 221 | if not line.strip(): continue 222 | 223 | m = self._name_rgx.match(line) 224 | if m and line[m.end():].strip().startswith(':'): 225 | push_item(current_func, rest) 226 | current_func, line = line[:m.end()], line[m.end():] 227 | rest = [line.split(':', 1)[1].strip()] 228 | if not rest[0]: 229 | rest = [] 230 | elif not line.startswith(' '): 231 | push_item(current_func, rest) 232 | current_func = None 233 | if ',' in line: 234 | for func in line.split(','): 235 | push_item(func, []) 236 | elif line.strip(): 237 | current_func = line 238 | elif current_func is not None: 239 | rest.append(line.strip()) 240 | push_item(current_func, rest) 241 | return items 242 | 243 | def _parse_index(self, section, content): 244 | """ 245 | .. index: default 246 | :refguide: something, else, and more 247 | 248 | """ 249 | def strip_each_in(lst): 250 | return [s.strip() for s in lst] 251 | 252 | out = {} 253 | section = section.split('::') 254 | if len(section) > 1: 255 | out['default'] = strip_each_in(section[1].split(','))[0] 256 | for line in content: 257 | line = line.split(':') 258 | if len(line) > 2: 259 | out[line[1]] = strip_each_in(line[2].split(',')) 260 | return out 261 | 262 | def _parse_summary(self): 263 | """Grab signature (if given) and summary""" 264 | if self._is_at_section(): 265 | return 266 | 267 | summary = self._doc.read_to_next_empty_line() 268 | summary_str = " ".join([s.strip() for s in summary]).strip() 269 | if re.compile('^([\w., ]+=)?\s*[\w\.]+\(.*\)$').match(summary_str): 270 | self['Signature'] = summary_str 271 | if not self._is_at_section(): 272 | self['Summary'] = self._doc.read_to_next_empty_line() 273 | else: 274 | self['Summary'] = summary 275 | 276 | if not self._is_at_section(): 277 | self['Extended Summary'] = self._read_to_next_section() 278 | 279 | def _parse(self): 280 | self._doc.reset() 281 | self._parse_summary() 282 | 283 | for (section,content) in self._read_sections(): 284 | if not section.startswith('..'): 285 | section = ' '.join([s.capitalize() for s in section.split(' ')]) 286 | if section in ('Parameters', 'Attributes', 'Methods', 287 | 'Returns', 'Raises', 'Warns'): 288 | self[section] = self._parse_param_list(content) 289 | elif section.startswith('.. index::'): 290 | self['index'] = self._parse_index(section, content) 291 | elif section == 'See Also': 292 | self['See Also'] = self._parse_see_also(content) 293 | else: 294 | self[section] = content 295 | 296 | # string conversion routines 297 | 298 | def _str_header(self, name, symbol='-'): 299 | return [name, len(name)*symbol] 300 | 301 | def _str_indent(self, doc, indent=4): 302 | out = [] 303 | for line in doc: 304 | out += [' '*indent + line] 305 | return out 306 | 307 | def _str_signature(self): 308 | if self['Signature']: 309 | return [self['Signature'].replace('*','\*')] + [''] 310 | else: 311 | return [''] 312 | 313 | def _str_summary(self): 314 | if self['Summary']: 315 | return self['Summary'] + [''] 316 | else: 317 | return [] 318 | 319 | def _str_extended_summary(self): 320 | if self['Extended Summary']: 321 | return self['Extended Summary'] + [''] 322 | else: 323 | return [] 324 | 325 | def _str_param_list(self, name): 326 | out = [] 327 | if self[name]: 328 | out += self._str_header(name) 329 | for param,param_type,desc in self[name]: 330 | out += ['%s : %s' % (param, param_type)] 331 | out += self._str_indent(desc) 332 | out += [''] 333 | return out 334 | 335 | def _str_section(self, name): 336 | out = [] 337 | if self[name]: 338 | out += self._str_header(name) 339 | out += self[name] 340 | out += [''] 341 | return out 342 | 343 | def _str_see_also(self, func_role): 344 | if not self['See Also']: return [] 345 | out = [] 346 | out += self._str_header("See Also") 347 | last_had_desc = True 348 | for func, desc, role in self['See Also']: 349 | if role: 350 | link = ':%s:`%s`' % (role, func) 351 | elif func_role: 352 | link = ':%s:`%s`' % (func_role, func) 353 | else: 354 | link = "`%s`_" % func 355 | if desc or last_had_desc: 356 | out += [''] 357 | out += [link] 358 | else: 359 | out[-1] += ", %s" % link 360 | if desc: 361 | out += self._str_indent([' '.join(desc)]) 362 | last_had_desc = True 363 | else: 364 | last_had_desc = False 365 | out += [''] 366 | return out 367 | 368 | def _str_index(self): 369 | idx = self['index'] 370 | out = [] 371 | out += ['.. index:: %s' % idx.get('default','')] 372 | for section, references in idx.iteritems(): 373 | if section == 'default': 374 | continue 375 | out += [' :%s: %s' % (section, ', '.join(references))] 376 | return out 377 | 378 | def __str__(self, func_role=''): 379 | out = [] 380 | out += self._str_signature() 381 | out += self._str_summary() 382 | out += self._str_extended_summary() 383 | for param_list in ('Parameters','Returns','Raises'): 384 | out += self._str_param_list(param_list) 385 | out += self._str_section('Warnings') 386 | out += self._str_see_also(func_role) 387 | for s in ('Notes','References','Examples'): 388 | out += self._str_section(s) 389 | out += self._str_index() 390 | return '\n'.join(out) 391 | 392 | 393 | def indent(str,indent=4): 394 | indent_str = ' '*indent 395 | if str is None: 396 | return indent_str 397 | lines = str.split('\n') 398 | return '\n'.join(indent_str + l for l in lines) 399 | 400 | def dedent_lines(lines): 401 | """Deindent a list of lines maximally""" 402 | return textwrap.dedent("\n".join(lines)).split("\n") 403 | 404 | def header(text, style='-'): 405 | return text + '\n' + style*len(text) + '\n' 406 | 407 | 408 | class FunctionDoc(NumpyDocString): 409 | def __init__(self, func, role='func', doc=None): 410 | self._f = func 411 | self._role = role # e.g. "func" or "meth" 412 | if doc is None: 413 | doc = inspect.getdoc(func) or '' 414 | try: 415 | NumpyDocString.__init__(self, doc) 416 | except ValueError, e: 417 | print '*'*78 418 | print "ERROR: '%s' while parsing `%s`" % (e, self._f) 419 | print '*'*78 420 | #print "Docstring follows:" 421 | #print doclines 422 | #print '='*78 423 | 424 | if not self['Signature']: 425 | func, func_name = self.get_func() 426 | try: 427 | # try to read signature 428 | argspec = inspect.getargspec(func) 429 | argspec = inspect.formatargspec(*argspec) 430 | argspec = argspec.replace('*','\*') 431 | signature = '%s%s' % (func_name, argspec) 432 | except TypeError, e: 433 | signature = '%s()' % func_name 434 | self['Signature'] = signature 435 | 436 | def get_func(self): 437 | func_name = getattr(self._f, '__name__', self.__class__.__name__) 438 | if inspect.isclass(self._f): 439 | func = getattr(self._f, '__call__', self._f.__init__) 440 | else: 441 | func = self._f 442 | return func, func_name 443 | 444 | def __str__(self): 445 | out = '' 446 | 447 | func, func_name = self.get_func() 448 | signature = self['Signature'].replace('*', '\*') 449 | 450 | roles = {'func': 'function', 451 | 'meth': 'method'} 452 | 453 | if self._role: 454 | if not roles.has_key(self._role): 455 | print "Warning: invalid role %s" % self._role 456 | out += '.. %s:: %s\n \n\n' % (roles.get(self._role,''), 457 | func_name) 458 | 459 | out += super(FunctionDoc, self).__str__(func_role=self._role) 460 | return out 461 | 462 | 463 | class ClassDoc(NumpyDocString): 464 | def __init__(self,cls,modulename='',func_doc=FunctionDoc,doc=None): 465 | if not inspect.isclass(cls): 466 | raise ValueError("Initialise using a class. Got %r" % cls) 467 | self._cls = cls 468 | 469 | if modulename and not modulename.endswith('.'): 470 | modulename += '.' 471 | self._mod = modulename 472 | self._name = cls.__name__ 473 | self._func_doc = func_doc 474 | 475 | if doc is None: 476 | doc = pydoc.getdoc(cls) 477 | 478 | NumpyDocString.__init__(self, doc) 479 | 480 | @property 481 | def methods(self): 482 | return [name for name,func in inspect.getmembers(self._cls) 483 | if not name.startswith('_') and callable(func)] 484 | 485 | def __str__(self): 486 | out = '' 487 | out += super(ClassDoc, self).__str__() 488 | out += "\n\n" 489 | 490 | #for m in self.methods: 491 | # print "Parsing `%s`" % m 492 | # out += str(self._func_doc(getattr(self._cls,m), 'meth')) + '\n\n' 493 | # out += '.. index::\n single: %s; %s\n\n' % (self._name, m) 494 | 495 | return out 496 | 497 | 498 | -------------------------------------------------------------------------------- /docs/sphinxext/apigen.py: -------------------------------------------------------------------------------- 1 | """Attempt to generate templates for module reference with Sphinx 2 | 3 | XXX - we exclude extension modules 4 | 5 | To include extension modules, first identify them as valid in the 6 | ``_uri2path`` method, then handle them in the ``_parse_module`` script. 7 | 8 | We get functions and classes by parsing the text of .py files. 9 | Alternatively we could import the modules for discovery, and we'd have 10 | to do that for extension modules. This would involve changing the 11 | ``_parse_module`` method to work via import and introspection, and 12 | might involve changing ``discover_modules`` (which determines which 13 | files are modules, and therefore which module URIs will be passed to 14 | ``_parse_module``). 15 | 16 | NOTE: this is a modified version of a script originally shipped with the 17 | PyMVPA project, which we've adapted for NIPY use. PyMVPA is an MIT-licensed 18 | project.""" 19 | 20 | # Stdlib imports 21 | import os 22 | import re 23 | 24 | # Functions and classes 25 | class ApiDocWriter(object): 26 | ''' Class for automatic detection and parsing of API docs 27 | to Sphinx-parsable reST format''' 28 | 29 | # only separating first two levels 30 | rst_section_levels = ['*', '=', '-', '~', '^'] 31 | 32 | def __init__(self, 33 | package_name, 34 | rst_extension='.rst', 35 | package_skip_patterns=None, 36 | module_skip_patterns=None, 37 | ): 38 | ''' Initialize package for parsing 39 | 40 | Parameters 41 | ---------- 42 | package_name : string 43 | Name of the top-level package. *package_name* must be the 44 | name of an importable package 45 | rst_extension : string, optional 46 | Extension for reST files, default '.rst' 47 | package_skip_patterns : None or sequence of {strings, regexps} 48 | Sequence of strings giving URIs of packages to be excluded 49 | Operates on the package path, starting at (including) the 50 | first dot in the package path, after *package_name* - so, 51 | if *package_name* is ``sphinx``, then ``sphinx.util`` will 52 | result in ``.util`` being passed for earching by these 53 | regexps. If is None, gives default. Default is: 54 | ['\.tests$'] 55 | module_skip_patterns : None or sequence 56 | Sequence of strings giving URIs of modules to be excluded 57 | Operates on the module name including preceding URI path, 58 | back to the first dot after *package_name*. For example 59 | ``sphinx.util.console`` results in the string to search of 60 | ``.util.console`` 61 | If is None, gives default. Default is: 62 | ['\.setup$', '\._'] 63 | ''' 64 | if package_skip_patterns is None: 65 | package_skip_patterns = ['\\.tests$'] 66 | if module_skip_patterns is None: 67 | module_skip_patterns = ['\\.setup$', '\\._'] 68 | self.package_name = package_name 69 | self.rst_extension = rst_extension 70 | self.package_skip_patterns = package_skip_patterns 71 | self.module_skip_patterns = module_skip_patterns 72 | 73 | def get_package_name(self): 74 | return self._package_name 75 | 76 | def set_package_name(self, package_name): 77 | ''' Set package_name 78 | 79 | >>> docwriter = ApiDocWriter('sphinx') 80 | >>> import sphinx 81 | >>> docwriter.root_path == sphinx.__path__[0] 82 | True 83 | >>> docwriter.package_name = 'docutils' 84 | >>> import docutils 85 | >>> docwriter.root_path == docutils.__path__[0] 86 | True 87 | ''' 88 | # It's also possible to imagine caching the module parsing here 89 | self._package_name = package_name 90 | self.root_module = __import__(package_name) 91 | self.root_path = self.root_module.__path__[0] 92 | self.written_modules = None 93 | 94 | package_name = property(get_package_name, set_package_name, None, 95 | 'get/set package_name') 96 | 97 | def _get_object_name(self, line): 98 | ''' Get second token in line 99 | >>> docwriter = ApiDocWriter('sphinx') 100 | >>> docwriter._get_object_name(" def func(): ") 101 | 'func' 102 | >>> docwriter._get_object_name(" class Klass(object): ") 103 | 'Klass' 104 | >>> docwriter._get_object_name(" class Klass: ") 105 | 'Klass' 106 | ''' 107 | name = line.split()[1].split('(')[0].strip() 108 | # in case we have classes which are not derived from object 109 | # ie. old style classes 110 | return name.rstrip(':') 111 | 112 | def _uri2path(self, uri): 113 | ''' Convert uri to absolute filepath 114 | 115 | Parameters 116 | ---------- 117 | uri : string 118 | URI of python module to return path for 119 | 120 | Returns 121 | ------- 122 | path : None or string 123 | Returns None if there is no valid path for this URI 124 | Otherwise returns absolute file system path for URI 125 | 126 | Examples 127 | -------- 128 | >>> docwriter = ApiDocWriter('sphinx') 129 | >>> import sphinx 130 | >>> modpath = sphinx.__path__[0] 131 | >>> res = docwriter._uri2path('sphinx.builder') 132 | >>> res == os.path.join(modpath, 'builder.py') 133 | True 134 | >>> res = docwriter._uri2path('sphinx') 135 | >>> res == os.path.join(modpath, '__init__.py') 136 | True 137 | >>> docwriter._uri2path('sphinx.does_not_exist') 138 | 139 | ''' 140 | if uri == self.package_name: 141 | return os.path.join(self.root_path, '__init__.py') 142 | path = uri.replace('.', os.path.sep) 143 | path = path.replace(self.package_name + os.path.sep, '') 144 | path = os.path.join(self.root_path, path) 145 | # XXX maybe check for extensions as well? 146 | if os.path.exists(path + '.py'): # file 147 | path += '.py' 148 | elif os.path.exists(os.path.join(path, '__init__.py')): 149 | path = os.path.join(path, '__init__.py') 150 | else: 151 | return None 152 | return path 153 | 154 | def _path2uri(self, dirpath): 155 | ''' Convert directory path to uri ''' 156 | relpath = dirpath.replace(self.root_path, self.package_name) 157 | if relpath.startswith(os.path.sep): 158 | relpath = relpath[1:] 159 | return relpath.replace(os.path.sep, '.') 160 | 161 | def _parse_module(self, uri): 162 | ''' Parse module defined in *uri* ''' 163 | filename = self._uri2path(uri) 164 | if filename is None: 165 | # nothing that we could handle here. 166 | return ([],[]) 167 | f = open(filename, 'rt') 168 | functions, classes = self._parse_lines(f) 169 | f.close() 170 | return functions, classes 171 | 172 | def _parse_lines(self, linesource): 173 | ''' Parse lines of text for functions and classes ''' 174 | functions = [] 175 | classes = [] 176 | for line in linesource: 177 | if line.startswith('def ') and line.count('('): 178 | # exclude private stuff 179 | name = self._get_object_name(line) 180 | if not name.startswith('_'): 181 | functions.append(name) 182 | elif line.startswith('class '): 183 | # exclude private stuff 184 | name = self._get_object_name(line) 185 | if not name.startswith('_'): 186 | classes.append(name) 187 | else: 188 | pass 189 | functions.sort() 190 | classes.sort() 191 | return functions, classes 192 | 193 | def generate_api_doc(self, uri): 194 | '''Make autodoc documentation template string for a module 195 | 196 | Parameters 197 | ---------- 198 | uri : string 199 | python location of module - e.g 'sphinx.builder' 200 | 201 | Returns 202 | ------- 203 | S : string 204 | Contents of API doc 205 | ''' 206 | # get the names of all classes and functions 207 | functions, classes = self._parse_module(uri) 208 | if not len(functions) and not len(classes): 209 | print 'WARNING: Empty -',uri # dbg 210 | return '' 211 | 212 | # Make a shorter version of the uri that omits the package name for 213 | # titles 214 | uri_short = re.sub(r'^%s\.' % self.package_name,'',uri) 215 | 216 | ad = '.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n' 217 | 218 | chap_title = uri_short 219 | ad += (chap_title+'\n'+ self.rst_section_levels[1] * len(chap_title) 220 | + '\n\n') 221 | 222 | # Set the chapter title to read 'module' for all modules except for the 223 | # main packages 224 | if '.' in uri: 225 | title = 'Module: :mod:`' + uri_short + '`' 226 | else: 227 | title = ':mod:`' + uri_short + '`' 228 | ad += title + '\n' + self.rst_section_levels[2] * len(title) 229 | 230 | if len(classes): 231 | ad += '\nInheritance diagram for ``%s``:\n\n' % uri 232 | ad += '.. inheritance-diagram:: %s \n' % uri 233 | ad += ' :parts: 3\n' 234 | 235 | ad += '\n.. automodule:: ' + uri + '\n' 236 | ad += '\n.. currentmodule:: ' + uri + '\n' 237 | multi_class = len(classes) > 1 238 | multi_fx = len(functions) > 1 239 | if multi_class: 240 | ad += '\n' + 'Classes' + '\n' + \ 241 | self.rst_section_levels[2] * 7 + '\n' 242 | elif len(classes) and multi_fx: 243 | ad += '\n' + 'Class' + '\n' + \ 244 | self.rst_section_levels[2] * 5 + '\n' 245 | for c in classes: 246 | ad += '\n:class:`' + c + '`\n' \ 247 | + self.rst_section_levels[multi_class + 2 ] * \ 248 | (len(c)+9) + '\n\n' 249 | ad += '\n.. autoclass:: ' + c + '\n' 250 | # must NOT exclude from index to keep cross-refs working 251 | ad += ' :members:\n' \ 252 | ' :undoc-members:\n' \ 253 | ' :show-inheritance:\n' \ 254 | ' :inherited-members:\n' \ 255 | '\n' \ 256 | ' .. automethod:: __init__\n' 257 | if multi_fx: 258 | ad += '\n' + 'Functions' + '\n' + \ 259 | self.rst_section_levels[2] * 9 + '\n\n' 260 | elif len(functions) and multi_class: 261 | ad += '\n' + 'Function' + '\n' + \ 262 | self.rst_section_levels[2] * 8 + '\n\n' 263 | for f in functions: 264 | # must NOT exclude from index to keep cross-refs working 265 | ad += '\n.. autofunction:: ' + uri + '.' + f + '\n\n' 266 | return ad 267 | 268 | def _survives_exclude(self, matchstr, match_type): 269 | ''' Returns True if *matchstr* does not match patterns 270 | 271 | ``self.package_name`` removed from front of string if present 272 | 273 | Examples 274 | -------- 275 | >>> dw = ApiDocWriter('sphinx') 276 | >>> dw._survives_exclude('sphinx.okpkg', 'package') 277 | True 278 | >>> dw.package_skip_patterns.append('^\\.badpkg$') 279 | >>> dw._survives_exclude('sphinx.badpkg', 'package') 280 | False 281 | >>> dw._survives_exclude('sphinx.badpkg', 'module') 282 | True 283 | >>> dw._survives_exclude('sphinx.badmod', 'module') 284 | True 285 | >>> dw.module_skip_patterns.append('^\\.badmod$') 286 | >>> dw._survives_exclude('sphinx.badmod', 'module') 287 | False 288 | ''' 289 | if match_type == 'module': 290 | patterns = self.module_skip_patterns 291 | elif match_type == 'package': 292 | patterns = self.package_skip_patterns 293 | else: 294 | raise ValueError('Cannot interpret match type "%s"' 295 | % match_type) 296 | # Match to URI without package name 297 | L = len(self.package_name) 298 | if matchstr[:L] == self.package_name: 299 | matchstr = matchstr[L:] 300 | for pat in patterns: 301 | try: 302 | pat.search 303 | except AttributeError: 304 | pat = re.compile(pat) 305 | if pat.search(matchstr): 306 | return False 307 | return True 308 | 309 | def discover_modules(self): 310 | ''' Return module sequence discovered from ``self.package_name`` 311 | 312 | 313 | Parameters 314 | ---------- 315 | None 316 | 317 | Returns 318 | ------- 319 | mods : sequence 320 | Sequence of module names within ``self.package_name`` 321 | 322 | Examples 323 | -------- 324 | >>> dw = ApiDocWriter('sphinx') 325 | >>> mods = dw.discover_modules() 326 | >>> 'sphinx.util' in mods 327 | True 328 | >>> dw.package_skip_patterns.append('\.util$') 329 | >>> 'sphinx.util' in dw.discover_modules() 330 | False 331 | >>> 332 | ''' 333 | modules = [self.package_name] 334 | # raw directory parsing 335 | for dirpath, dirnames, filenames in os.walk(self.root_path): 336 | # Check directory names for packages 337 | root_uri = self._path2uri(os.path.join(self.root_path, 338 | dirpath)) 339 | for dirname in dirnames[:]: # copy list - we modify inplace 340 | package_uri = '.'.join((root_uri, dirname)) 341 | if (self._uri2path(package_uri) and 342 | self._survives_exclude(package_uri, 'package')): 343 | modules.append(package_uri) 344 | else: 345 | dirnames.remove(dirname) 346 | # Check filenames for modules 347 | for filename in filenames: 348 | module_name = filename[:-3] 349 | module_uri = '.'.join((root_uri, module_name)) 350 | if (self._uri2path(module_uri) and 351 | self._survives_exclude(module_uri, 'module')): 352 | modules.append(module_uri) 353 | return sorted(modules) 354 | 355 | def write_modules_api(self, modules,outdir): 356 | # write the list 357 | written_modules = [] 358 | for m in modules: 359 | api_str = self.generate_api_doc(m) 360 | if not api_str: 361 | continue 362 | # write out to file 363 | outfile = os.path.join(outdir, 364 | m + self.rst_extension) 365 | fileobj = open(outfile, 'wt') 366 | fileobj.write(api_str) 367 | fileobj.close() 368 | written_modules.append(m) 369 | self.written_modules = written_modules 370 | 371 | def write_api_docs(self, outdir): 372 | """Generate API reST files. 373 | 374 | Parameters 375 | ---------- 376 | outdir : string 377 | Directory name in which to store files 378 | We create automatic filenames for each module 379 | 380 | Returns 381 | ------- 382 | None 383 | 384 | Notes 385 | ----- 386 | Sets self.written_modules to list of written modules 387 | """ 388 | if not os.path.exists(outdir): 389 | os.mkdir(outdir) 390 | # compose list of modules 391 | modules = self.discover_modules() 392 | self.write_modules_api(modules,outdir) 393 | 394 | def write_index(self, outdir, froot='gen', relative_to=None): 395 | """Make a reST API index file from written files 396 | 397 | Parameters 398 | ---------- 399 | path : string 400 | Filename to write index to 401 | outdir : string 402 | Directory to which to write generated index file 403 | froot : string, optional 404 | root (filename without extension) of filename to write to 405 | Defaults to 'gen'. We add ``self.rst_extension``. 406 | relative_to : string 407 | path to which written filenames are relative. This 408 | component of the written file path will be removed from 409 | outdir, in the generated index. Default is None, meaning, 410 | leave path as it is. 411 | """ 412 | if self.written_modules is None: 413 | raise ValueError('No modules written') 414 | # Get full filename path 415 | path = os.path.join(outdir, froot+self.rst_extension) 416 | # Path written into index is relative to rootpath 417 | if relative_to is not None: 418 | relpath = outdir.replace(relative_to + os.path.sep, '') 419 | else: 420 | relpath = outdir 421 | idx = open(path,'wt') 422 | w = idx.write 423 | w('.. AUTO-GENERATED FILE -- DO NOT EDIT!\n\n') 424 | w('.. toctree::\n\n') 425 | for f in self.written_modules: 426 | w(' %s\n' % os.path.join(relpath,f)) 427 | idx.close() 428 | --------------------------------------------------------------------------------