├── utils ├── __init__.py ├── date.py ├── market.py ├── progress_bar.py ├── model.py └── YahooQuote.py ├── .gitignore ├── indicators ├── __init__.py ├── __init__.pyc ├── price.py ├── simplevalue.py ├── sma.py ├── ema.py └── rsi.py ├── pycommando ├── __init__.py ├── .gitignore ├── example.py └── commando.py ├── scripts └── example.txt ├── strategies ├── hold.py ├── sell.py ├── strategy.py └── trending.py ├── portfolio.py ├── config.py ├── README.rst ├── plots.py ├── yahoo.py ├── quant ├── report.py ├── simulation.py └── LICENSE /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /indicators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pycommando/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pycommando/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | -------------------------------------------------------------------------------- /indicators/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maihde/quant/HEAD/indicators/__init__.pyc -------------------------------------------------------------------------------- /scripts/example.txt: -------------------------------------------------------------------------------- 1 | ! Start lines with an exclimation point create comments 2 | ! 3 | ! You don't need to delete/create portfolios everytime, as they are stored in ~/quant/quant.cfg 4 | ! You can also directly edit quant.cfg instead of using the portfolio commands. 5 | portfolio_delete spy 6 | portfolio_create spy 0.0 '{spy: $10000}' 7 | simulate hold spy 2005-01-01 2010-01-01 ~/.quant/benchmark.h5 8 | simulate trending spy 2005-01-01 2010-01-01 ~/.quant/trending.h5 '{long: 50, short: 10}' 9 | report_performance ~/.quant/benchmark.h5 10 | report_performance ~/.quant/trending.h5 11 | plot_indicators spy all ~/.quant/trending.h5 12 | plot ~/.quant/trending.h5 13 | show 14 | -------------------------------------------------------------------------------- /utils/date.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | import datetime 20 | 21 | ONE_DAY = datetime.timedelta(days=1) 22 | -------------------------------------------------------------------------------- /strategies/hold.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: sw=4: et 3 | LICENSE=""" 4 | Copyright (C) 2011 Michael Ihde 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | """ 20 | from strategy import Strategy 21 | from utils.model import Order 22 | 23 | class Hold(Strategy): 24 | """The most basic strategy. It holds the initial postion and 25 | makes no trades....ever...for any reason. 26 | """ 27 | def evaluate(self, date, position, market): 28 | return () # There are never any orders from this strategy 29 | 30 | CLAZZ = Hold 31 | -------------------------------------------------------------------------------- /strategies/sell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: sw=4: et 3 | LICENSE=""" 4 | Copyright (C) 2011 Michael Ihde 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | """ 20 | from strategy import Strategy 21 | from utils.model import Order 22 | 23 | class SellOff(Strategy): 24 | """A simple strategy, useful for testing, that sells everything immediately.""" 25 | def evaluate(self, date, position, market): 26 | orders = [] 27 | for symbol, p in position.items(): 28 | if symbol != '$' and p.amount > 0: 29 | orders.append(Order(Order.SELL, symbol, p.amount, Order.MARKET_PRICE)) 30 | return orders 31 | 32 | CLAZZ = SellOff 33 | -------------------------------------------------------------------------------- /indicators/price.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | import tables 20 | 21 | class ValueData(tables.IsDescription): 22 | date = tables.TimeCol() 23 | value = tables.Float32Col() 24 | 25 | class SimpleValue(object): 26 | def __init__(self): 27 | self.value = 0.0 28 | self.tbl = None 29 | 30 | def setupH5(self, h5file, h5where, h5name): 31 | if h5file != None and h5where != None and h5name != None: 32 | self.tbl = h5file.createTable(h5where, h5name, ValueData) 33 | 34 | def update(self, value, date=None): 35 | self.value = value 36 | if self.tbl != None and date: 37 | self.tbl.row["date"] = date.date().toordinal() 38 | self.tbl.row["value"] = self.value 39 | self.tbl.row.append() 40 | self.tbl.flush() 41 | 42 | return self.value 43 | -------------------------------------------------------------------------------- /indicators/simplevalue.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | import tables 20 | 21 | class ValueData(tables.IsDescription): 22 | date = tables.TimeCol() 23 | value = tables.Float32Col() 24 | 25 | class SimpleValue(object): 26 | def __init__(self): 27 | self.value = 0.0 28 | self.tbl = None 29 | 30 | def setupH5(self, h5file, h5where, h5name): 31 | if h5file != None and h5where != None and h5name != None: 32 | self.tbl = h5file.createTable(h5where, h5name, ValueData) 33 | 34 | def update(self, value, date=None): 35 | self.value = value 36 | if self.tbl != None and date: 37 | self.tbl.row["date"] = date.date().toordinal() 38 | self.tbl.row["value"] = self.value 39 | self.tbl.row.append() 40 | self.tbl.flush() 41 | 42 | return self.value 43 | -------------------------------------------------------------------------------- /pycommando/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: sw=4: et: 3 | import readline 4 | import sys 5 | from commando import * 6 | 7 | # Define a command called "action1" 8 | @command("action1") 9 | def action1(): 10 | """Do something""" 11 | print "action1" 12 | 13 | # Define a command called "doit" 14 | @command("doit", prompts=(("value2", "Enter a number", int), 15 | ("value1", "Enter a string", str))) 16 | def action2(value1, value2=5): 17 | """Do something else""" 18 | print "action2", repr(value1), repr(value2) 19 | 20 | # Define a command called "go" using default prompts 21 | @command("go") 22 | def action3(value1=False): 23 | """Do another thing 24 | but do it well""" 25 | print "action3", repr(value1) 26 | 27 | # Define multiple commands that call the same function 28 | @command("exit") 29 | @command("quit") 30 | def exit(): 31 | """Quit""" 32 | sys.exit(0) 33 | 34 | commando = Commando() 35 | commando.cmdloop() 36 | # Some examples 37 | # (Cmd) doit 38 | # Enter a string: abc 39 | # Enter a number [5]: 6 40 | # action2 'abc' 6 41 | # (Cmd) doit def 42 | # Enter a number [5]: 43 | # action2 'def' 5 44 | # (Cmd) doit abc ,, 45 | # action2 'abc' 5 46 | # (Cmd) doit ,,, 47 | # Enter a string: abc 48 | # action2 'abc' 5 49 | # (Cmd) go 50 | # Enter value1 Y/[N]: 51 | # action3 False 52 | # (Cmd) go 53 | # Enter value1 Y/[N]: Y 54 | # action3 True 55 | # (Cmd) go Y 56 | # action3 True 57 | # (Cmd) go N 58 | # action3 False 59 | # (Cmd) go True 60 | # action3 True 61 | # (Cmd) go False 62 | # action3 False 63 | # (Cmd) go No 64 | # action3 False 65 | # (Cmd) go Yes 66 | # action3 True 67 | -------------------------------------------------------------------------------- /utils/market.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | from yahoo import Market 20 | from utils.date import ONE_DAY 21 | import datetime 22 | 23 | MARKET = Market() 24 | 25 | def isTradingDay(date): 26 | if date.weekday() in (0,1,2,3,4): 27 | # Consider any day the Dow was active as a trading day 28 | ticker = MARKET["^DJI"] 29 | if (ticker != None): 30 | quote = ticker[date] 31 | if quote.adjclose != None: 32 | return True 33 | return False 34 | 35 | 36 | def getPrevTradingDay(date): 37 | prev_trading_day = date - ONE_DAY 38 | while isTradingDay(prev_trading_day) == False: 39 | prev_trading_day = prev_trading_day - ONE_DAY 40 | return prev_trading_day 41 | 42 | def getNextTradingDay(date): 43 | next_trading_day = date + ONE_DAY 44 | while isTradingDay(next_trading_day) == False: 45 | next_trading_day = next_trading_day - ONE_DAY 46 | return next_trading_day 47 | -------------------------------------------------------------------------------- /portfolio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | LICENSE=""" 4 | Copyright (C) 2011 Michael Ihde 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | """ 20 | from pycommando.commando import command 21 | from config import CONFIG 22 | from utils.date import ONE_DAY 23 | import yahoo 24 | import datetime 25 | import math 26 | import yaml 27 | 28 | @command("portfolio_create") 29 | def create(name, cash_percent=0.0, initial_position="{}"): 30 | if not CONFIG["portfolios"].has_key(name): 31 | CONFIG["portfolios"][name] = {} 32 | CONFIG["portfolios"][name]['$'] = cash_percent 33 | initial_position = yaml.load(initial_position) 34 | for sym, amt in initial_position.items(): 35 | CONFIG["portfolios"][name][sym] = amt 36 | CONFIG.commit() 37 | else: 38 | raise StandardError, "Portfolio already exists" 39 | 40 | @command("portfolio_delete") 41 | def delete(name): 42 | if CONFIG["portfolios"].has_key(name): 43 | del CONFIG['portfolios'][name] 44 | CONFIG.commit() 45 | 46 | @command("portfolio_risk") 47 | def risk(portfolio, start, end, initial_value=10000.0): 48 | # Emulate risk reports found in beancounter 49 | # TODO 50 | raise NotImplementedError 51 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | 20 | from pycommando.commando import command 21 | import os 22 | import yaml 23 | 24 | QUANT_DIR=os.path.expanduser("~/.quant") 25 | 26 | @command("config") 27 | def getConfig(): 28 | return CONFIG 29 | 30 | class _Config(object): 31 | __CONFIG_FILE = os.path.join(QUANT_DIR, "quant.cfg") 32 | __YAML = None 33 | 34 | def __init__(self): 35 | self.load() 36 | 37 | def has_key(self, index): 38 | return _Config.__YAML.has_key(index) 39 | 40 | def __getitem__(self, index): 41 | return _Config.__YAML[index] 42 | 43 | def __str__(self): 44 | return yaml.dump(_Config.__YAML) 45 | 46 | def load(self): 47 | try: 48 | _Config.__YAML = yaml.load(open(_Config.__CONFIG_FILE)) 49 | except IOError: 50 | pass 51 | # If a configuration is empty or didn't load, create a sample portfolio 52 | if _Config.__YAML == None: 53 | _Config.__YAML = {"portfolios": {"cash": {"$": 10000.0}}} 54 | self.commit() 55 | 56 | def commit(self): 57 | f = open(_Config.__CONFIG_FILE, "w") 58 | f.write(yaml.dump(_Config.__YAML)) 59 | f.close() 60 | 61 | # Create a global config singleton object 62 | CONFIG = _Config() 63 | -------------------------------------------------------------------------------- /utils/progress_bar.py: -------------------------------------------------------------------------------- 1 | LICENSE=""" 2 | Copyright (C) 2011 Michael Ihde 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU General Public License 6 | as published by the Free Software Foundation; either version 2 7 | of the License, or (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program; if not, write to the Free Software 16 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | """ 18 | 19 | class ProgressBar: 20 | def __init__(self, minValue = 0, maxValue = 10, totalWidth=12): 21 | self.progBar = "[]" # This holds the progress bar string 22 | self.min = minValue 23 | self.max = maxValue 24 | self.span = maxValue - minValue 25 | self.width = totalWidth 26 | self.amount = 0 # When amount == max, we are 100% done 27 | self.updateAmount(0) # Build progress bar string 28 | 29 | def performWork(self, work): 30 | self.updateAmount(self.amount + work) 31 | 32 | def updateAmount(self, newAmount = 0): 33 | if newAmount < self.min: newAmount = self.min 34 | if newAmount > self.max: newAmount = self.max 35 | self.amount = newAmount 36 | 37 | # Figure out the new percent done, round to an integer 38 | diffFromMin = float(self.amount - self.min) 39 | percentDone = (diffFromMin / float(self.span)) * 100.0 40 | percentDone = round(percentDone) 41 | percentDone = int(percentDone) 42 | 43 | # Figure out how many hash bars the percentage should be 44 | allFull = self.width - 2 45 | numHashes = (percentDone / 100.0) * allFull 46 | numHashes = int(round(numHashes)) 47 | 48 | # build a progress bar with hashes and spaces 49 | self.progBar = "[" + '#'*numHashes + ' '*(allFull-numHashes) + "]" 50 | 51 | # figure out where to put the percentage, roughly centered 52 | percentPlace = (len(self.progBar) / 2) - len(str(percentDone)) 53 | percentString = str(percentDone) + "%" 54 | 55 | # slice the percentage into the bar 56 | self.progBar = self.progBar[0:percentPlace] + percentString + self.progBar[percentPlace+len(percentString):] 57 | 58 | def __str__(self): 59 | return str(self.progBar) 60 | -------------------------------------------------------------------------------- /indicators/sma.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | import tables 20 | 21 | class SMAData(tables.IsDescription): 22 | date = tables.TimeCol() 23 | value = tables.Float32Col() 24 | 25 | class SMA(object): 26 | def __init__(self, period): 27 | self.values = [0.0 for x in xrange(period)] 28 | self.value = 0.0 29 | self.period = period 30 | self.tbl = None 31 | 32 | def setupH5(self, h5file, h5where, h5name): 33 | if h5file != None and h5where != None and h5name != None: 34 | self.tbl = h5file.createTable(h5where, h5name, SMAData) 35 | 36 | def update(self, value, date=None): 37 | oldest = self.values.pop() 38 | self.values.insert(0, value) 39 | 40 | self.value = self.value - (oldest / self.period) + (value / self.period) 41 | 42 | if self.tbl != None and date: 43 | self.tbl.row["date"] = date.date().toordinal() 44 | self.tbl.row["value"] = self.value 45 | self.tbl.row.append() 46 | self.tbl.flush() 47 | 48 | return self.value 49 | 50 | if __name__ == "__main__": 51 | import unittest 52 | 53 | class SMATest(unittest.TestCase): 54 | 55 | def test_ConstantValueAlgorithm(self): 56 | sma = SMA(5) 57 | self.assertEqual(sma.value, 0.0) 58 | 59 | sma.update(5.0) 60 | self.assertEqual(sma.value, 1.0) 61 | sma.update(5.0) 62 | self.assertEqual(sma.value, 2.0) 63 | sma.update(5.0) 64 | self.assertEqual(sma.value, 3.0) 65 | sma.update(5.0) 66 | self.assertEqual(sma.value, 4.0) 67 | sma.update(5.0) 68 | self.assertEqual(sma.value, 5.0) 69 | sma.update(5.0) 70 | self.assertEqual(sma.value, 5.0) 71 | sma.update(5.0) 72 | self.assertEqual(sma.value, 5.0) 73 | 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /indicators/ema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | import tables 20 | 21 | class EMAData(tables.IsDescription): 22 | date = tables.TimeCol() 23 | value = tables.Float32Col() 24 | 25 | class EMA(object): 26 | def __init__(self, period): 27 | self.value = None 28 | self.alpha = 2.0 / (period+1) 29 | self.tbl = None 30 | 31 | def setupH5(self, h5file, h5where, h5name): 32 | if h5file != None and h5where != None and h5name != None: 33 | self.tbl = h5file.createTable(h5where, h5name, EMAData) 34 | 35 | def update(self, value, date=None): 36 | if self.value == None: 37 | self.value = value 38 | else: 39 | self.value = (value * self.alpha) + (self.value * (1 - self.alpha)) 40 | if self.tbl != None and date: 41 | self.tbl.row["date"] = date.date().toordinal() 42 | self.tbl.row["value"] = self.value 43 | self.tbl.row.append() 44 | self.tbl.flush() 45 | 46 | return self.value 47 | 48 | if __name__ == "__main__": 49 | import unittest 50 | 51 | class EMATest(unittest.TestCase): 52 | 53 | def test_Alpha(self): 54 | ema = EMA(9) 55 | self.assertEqual(ema.alpha, 0.2) 56 | 57 | ema = EMA(19) 58 | self.assertEqual(ema.alpha, 0.1) 59 | 60 | def test_ConstantValueAlgorithm(self): 61 | ema = EMA(15) 62 | self.assertEqual(ema.value, None) 63 | for i in xrange(50): 64 | ema.update(5.) 65 | self.assertEqual(ema.value, 5.) 66 | 67 | def test_ConstantZeroValueAlgorithm(self): 68 | ema = EMA(10) 69 | self.assertEqual(ema.value, None) 70 | for i in xrange(50): 71 | ema.update(0.) 72 | self.assertEqual(ema.value, 0.) 73 | 74 | def test_ConstantZeroValueAlgorithm(self): 75 | ema = EMA(9) 76 | self.assertEqual(ema.value, None) 77 | ema.update(1.) 78 | self.assertEqual(ema.value, 1.) 79 | ema.update(2.) 80 | self.assertAlmostEqual(ema.value, 1.2) 81 | 82 | 83 | unittest.main() 84 | -------------------------------------------------------------------------------- /strategies/strategy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | from utils.date import ONE_DAY 20 | import tables 21 | 22 | class Strategy(object): 23 | def __init__(self, start_date, end_date, initial_position, market, params, h5file=None): 24 | self.start_date = start_date 25 | self.end_date = end_date 26 | self.initial_position = initial_position 27 | self.market = market 28 | self.params = params 29 | 30 | # Manage indicators, the dictionary is: 31 | # key = symbol 32 | # value = dictionary(key="indicator name", value=indicator) 33 | self.indicators = {} 34 | 35 | # If the strategy was passed h5 info, use it to store information 36 | self.h5file = h5file 37 | if h5file != None: 38 | self.indicator_h5group = h5file.createGroup("/", "Indicators") 39 | self.strategy_h5group = h5file.createGroup("/", "Strategy") 40 | 41 | def addIndicator(self, symbol, name, indicator): 42 | if not self.indicators.has_key(symbol): 43 | self.indicators[symbol] = {} 44 | self.indicators[symbol][name] = indicator 45 | 46 | if self.h5file != None: 47 | try: 48 | symgroup = self.h5file.getNode(self.indicator_h5group._v_pathname, symbol, classname="Group") 49 | except tables.NoSuchNodeError: 50 | symgroup = self.h5file.createGroup(self.indicator_h5group._v_pathname, symbol) 51 | 52 | if self.h5file and self.indicator_h5group: 53 | indicator.setupH5(self.h5file, symgroup, name) 54 | 55 | def removeIndicator(self, symbol, name): 56 | del self.indicators[symbol][name] 57 | 58 | def updateIndicators(self, start_date, end_date=None): 59 | for symbol, indicators in self.indicators.items(): 60 | ticker = self.market[symbol] 61 | if end_date != None: 62 | quotes = ticker[start_date:end_date] # Call this to cache everything 63 | end = end_date 64 | else: 65 | end = start_date + ONE_DAY 66 | 67 | d = start_date 68 | while d < end: 69 | quote = ticker[d] 70 | if quote.adjclose != None: 71 | for indicator in indicators.values(): 72 | indicator.update(quote.adjclose, d) 73 | d += ONE_DAY 74 | 75 | def evaluate(self, date, position): 76 | raise NotImplementedError 77 | 78 | def finalize(self): 79 | self.h5file = None 80 | self.indicator_h5group = None 81 | self.strategy_h5group = None 82 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Quant 3 | ======================= 4 | 5 | ------- 6 | License 7 | ------- 8 | Copyright (C) 2011 Michael Ihde 9 | 10 | This program is free software; you can redistribute it and/or modify it under 11 | the terms of the GNU General Public License as published by the Free Software 12 | Foundation; either version 2 of the License, or (at your option) any later 13 | version. 14 | 15 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 16 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 17 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License along with 20 | this program; if not, write to the Free Software Foundation, Inc., 59 Temple 21 | Place, Suite 330, Boston, MA 02111-1307 USA. 22 | 23 | ----- 24 | Intro 25 | ----- 26 | 27 | Quant is a python-based, technical analysis tool for trading strategies. It takes 28 | a particularily simplistic view of the market and only allows trading decisions to 29 | be made after the market has closed. 30 | 31 | The goal of Quant is to provide a useful experimentation framework to explore 32 | trading strategies that are applicable to the casual investor who is managing a 33 | 401k, IRA, or other long-term investment. 34 | 35 | Quant can be used without any programming knowledge, but a decent grasp of Python 36 | will be required to create customized strategies. 37 | 38 | ------- 39 | Install 40 | ------- 41 | 42 | Quant runs directly from it's own directory, as such Quant is not 'installed' 43 | in the usual sense. Quant does require a few libraries to be available 44 | 45 | On Ubuntu Linux:: 46 | sudo apt-get install python-tables python-sqlite python-matplotlib python-yaml 47 | 48 | --------------- 49 | Getting Started 50 | --------------- 51 | 52 | Quant stores it's configuration and all data in ~/.quant. 53 | 54 | All Quant portfolios are stored in ~/.quant/quant.cfg as a YAML entry. Entires 55 | in a portfolio can either be listed as absolute quantities or as cash values. 56 | The former is useful is you are using Quant to analyze a real portfolio that 57 | you currently own. The latter is useful for backtesting strategies as it 58 | ensures that your portfolio assets have proper ratios regardless of the 59 | assets price at the start of the simulation. 60 | 61 | Here is an example quant.cfg, the first portfolio uses cash values 62 | while the second portfolio uses absolute quantities:: 63 | portfolios: 64 | example_one: {$: 0, IWM: $30000, SPY: $45000, VWO: $25000} 65 | example_two: {$: 0, IWM: 30, SPY: 100, VWO: 50} 66 | 67 | Quant can be run in three modes: 68 | 69 | #. Interactive 70 | #. One-shot 71 | #. Scripted 72 | 73 | To run in interactive mode, simply execute quant:: 74 | $ ./quant 75 | 76 | To run in one-shot mode:: 77 | $ ./quant 78 | 79 | Finally, to run a script of commands:: 80 | $ ./quant < commands.txt 81 | 82 | An example script is provided in scripts/example.txt. It is suggested that you 83 | read it and then execute it:: 84 | $ ./quant < scripts/example.txt 85 | 86 | ---- 87 | Misc 88 | ---- 89 | 90 | If you are developing new strategies, you may want to install HDFView, as it will 91 | give you an easy way to explore the raw data in the output h5 files. HDFView can 92 | be downloaded from http://www.hdfgroup.org/hdf-java-html/hdfview/ 93 | 94 | You may also like to read these resources to learn more about Quantitive trading: 95 | 96 | - http://www.smartquant.com/introduction/openquant_strategy.pdf 97 | - Quantitative Trading: How to Build Your Own Algorithmic Trading Business by Ernest P. Chan 98 | -------------------------------------------------------------------------------- /plots.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | 20 | import os 21 | import sys 22 | import tables 23 | from utils.date import ONE_DAY 24 | from pycommando.commando import command 25 | import matplotlib.pyplot as plt 26 | import matplotlib.mlab as mlab 27 | import matplotlib.ticker as ticker 28 | import matplotlib.dates as dates 29 | 30 | @command("show") 31 | def show(): 32 | """Shows all plots that have been created, only necessary if you are 33 | creating plots in a script. Typically the last line of a script that 34 | creates plots will be 'show'. 35 | """ 36 | plt.show() 37 | 38 | @command("plot") 39 | def plot(input_="~/.quant/simulation.h5", node="/Performance", x="date", y="value"): 40 | try: 41 | inputFile = tables.openFile(os.path.expanduser(input_), "r") 42 | tbl = inputFile.getNode(node, classname="Table") 43 | 44 | x_data = tbl.col(x) 45 | y_data = tbl.col(y) 46 | finally: 47 | inputFile.close() 48 | 49 | fig = plt.figure() 50 | sp = fig.add_subplot(111) 51 | sp.set_title(input_) 52 | sp.plot(x_data, y_data, '-') 53 | 54 | x_locator = dates.AutoDateLocator() 55 | sp.xaxis.set_major_locator(x_locator) 56 | sp.xaxis.set_major_formatter(dates.AutoDateFormatter(x_locator)) 57 | 58 | #def format_date(value, pos=None): 59 | # return datetime.datetime.fromordinal(int(value)).strftime("%Y-%m-%d") 60 | #sp.xaxis.set_major_formatter(ticker.FuncFormatter(format_date)) 61 | fig.autofmt_xdate() 62 | fig.show() 63 | 64 | @command("plot_indicators") 65 | def plot_indicators(symbol="", indicator="all", input_="~/.quant/simulation.h5", x="date", y="value"): 66 | 67 | inputFile = tables.openFile(os.path.expanduser(input_), "r") 68 | try: 69 | symbols = [] 70 | if symbol == "": 71 | symbols = [grp._v_name for grp in inputFile.iterNodes("/Indicators", classname="Group")] 72 | else: 73 | symbols = (symbol,) 74 | 75 | for sym in symbols: 76 | fig = plt.figure() 77 | lines = [] 78 | legend = [] 79 | sp = fig.add_subplot(111) 80 | sp.set_title(input_ + " " + sym) 81 | for tbl in inputFile.iterNodes("/Indicators/" + sym, classname="Table"): 82 | if indicator == "all" or tbl._v_name == indicator: 83 | x_data = tbl.col(x) 84 | y_data = tbl.col(y) 85 | line = sp.plot(x_data, y_data, '-') 86 | lines.append(line) 87 | legend.append(tbl._v_name) 88 | x_locator = dates.AutoDateLocator() 89 | sp.xaxis.set_major_locator(x_locator) 90 | sp.xaxis.set_major_formatter(dates.AutoDateFormatter(x_locator)) 91 | legend = fig.legend(lines, legend, loc='upper right') 92 | fig.autofmt_xdate() 93 | fig.show() 94 | finally: 95 | inputFile.close() 96 | -------------------------------------------------------------------------------- /yahoo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | import os 20 | import datetime 21 | import matplotlib.pyplot as plt 22 | import matplotlib.mlab as mlab 23 | import matplotlib.ticker as ticker 24 | import matplotlib.dates as dates 25 | from pycommando.commando import command 26 | from utils.YahooQuote import * 27 | 28 | @command("db_ls") 29 | def list(): 30 | """ 31 | Lists the symbols that are in the database 32 | """ 33 | return Market().cache.symbols() 34 | 35 | @command("db_up") 36 | def update(symbol): 37 | """ 38 | Updates the historical daily prices for all stocks 39 | currently in the database. 40 | """ 41 | market = Market() 42 | try: 43 | ticker = market[symbol] 44 | if ticker != None: 45 | ticker.updateHistory() 46 | except IndexError: 47 | market.updateHistory() 48 | 49 | @command("db_flush") 50 | def flush(): 51 | """ 52 | Completely removes the yahoo cache 53 | """ 54 | os.remove(YahooQuote.CACHE) 55 | Market()._dbInit() 56 | 57 | @command("db_load") 58 | def load(symbol=None): 59 | """ 60 | Load's historical prices for a given ticker or index 61 | symbol from 1950 until today. This may take a long time, 62 | especially if you don't provide a symbol because it will 63 | cache all major indexes from 1950 until today. 64 | """ 65 | market = Market() 66 | if symbol == None: 67 | market.fetchHistory() 68 | else: 69 | ticker = market[symbol] 70 | if ticker != None: 71 | ticker.fetchHistory() 72 | 73 | @command("db_fetch") 74 | def fetch(symbol, start="today", end="today"): 75 | """ 76 | Prints the daily price for the stock on a given day. 77 | """ 78 | if start.upper() == "TODAY": 79 | day_start = datetime.date.today() 80 | else: 81 | day_start = datetime.datetime.strptime(start, "%Y-%m-%d") 82 | day_start = (day_start.year * 10000) + (day_start.month * 100) + day_start.day 83 | 84 | if end.upper() == "TODAY": 85 | day_end = None 86 | else: 87 | day_end = datetime.datetime.strptime(end, "%Y-%m-%d") 88 | day_end = (day_end.year * 10000) + (day_end.month * 100) + day_end.day 89 | 90 | ticker = Market()[symbol] 91 | if ticker != None: 92 | if day_end == None: 93 | return ticker[day_start] 94 | else: 95 | return ticker[day_start:day_end] 96 | 97 | @command("db_plot") 98 | def plot(symbol, start, end): 99 | """ 100 | Prints the daily price for the stock on a given day. 101 | """ 102 | 103 | quotes = fetch(symbol, start, end) 104 | x_data = [QuoteDate(q.date).toDateTime() for q in quotes] 105 | y_data = [q.adjclose for q in quotes] 106 | 107 | fig = plt.figure() 108 | fig.canvas.set_window_title("%s %s-%s" % (symbol, start, end)) 109 | sp = fig.add_subplot(111) 110 | sp.plot(x_data, y_data, '-') 111 | x_locator = dates.AutoDateLocator() 112 | sp.xaxis.set_major_locator(x_locator) 113 | sp.xaxis.set_major_formatter(dates.AutoDateFormatter(x_locator)) 114 | fig.autofmt_xdate() 115 | fig.show() 116 | -------------------------------------------------------------------------------- /indicators/rsi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | import tables 20 | import math 21 | from ema import EMA 22 | 23 | class RSIData(tables.IsDescription): 24 | date = tables.TimeCol() 25 | value = tables.Float32Col() 26 | 27 | class RSI(object): 28 | 29 | def __init__(self, period): 30 | self.value = None 31 | self.last = None 32 | self.ema_u = EMA(period) 33 | self.ema_d = EMA(period) 34 | self.tbl = None 35 | 36 | def setupH5(self, h5file, h5where, h5name): 37 | if h5file != None and h5where != None and h5name != None: 38 | self.tbl = h5file.createTable(h5where, h5name, RSIData) 39 | 40 | def update(self, value, date=None): 41 | if self.last == None: 42 | self.last = value 43 | 44 | U = value - self.last 45 | D = self.last - value 46 | 47 | self.last = value 48 | 49 | if U > 0: 50 | D = 0 51 | elif D > 0: 52 | U = 0 53 | 54 | self.ema_u.update(U) 55 | self.ema_d.update(D) 56 | 57 | if self.ema_d.value == 0: 58 | self.value = 100.0 59 | else: 60 | rs = self.ema_u.value / self.ema_d.value 61 | self.value = 100.0 - (100.0 / (1 + rs)) 62 | 63 | if self.tbl != None and date: 64 | self.tbl.row["date"] = date.date().toordinal() 65 | self.tbl.row["value"] = self.value 66 | self.tbl.row.append() 67 | self.tbl.flush() 68 | 69 | return self.value 70 | 71 | if __name__ == "__main__": 72 | import unittest 73 | 74 | class RSITest(unittest.TestCase): 75 | 76 | def notest_ConstantValueAlgorithm(self): 77 | rsi = RSI(14) 78 | self.assertEqual(rsi.value, None) 79 | self.assertEqual(rsi.last, None) 80 | for i in xrange(50): 81 | rsi.update(5.) 82 | self.assertEqual(rsi.value, 100.0) 83 | 84 | def notest_NoDAlgorithm(self): 85 | rsi = RSI(14) 86 | self.assertEqual(rsi.value, None) 87 | self.assertEqual(rsi.last, None) 88 | 89 | rsi.update(5.) # U = 0, D = 0 90 | self.assertEqual(rsi.value, 100.0) 91 | rsi.update(10.) # U = 5, D = 0 92 | self.assertEqual(rsi.value, 100.0) 93 | rsi.update(15.) # U = 5, D = 0 94 | self.assertEqual(rsi.value, 100.0) 95 | rsi.update(20.) # U = 5, D = 0 96 | self.assertEqual(rsi.value, 100.0) 97 | 98 | def notest_NoUAlgorithm(self): 99 | rsi = RSI(14) 100 | self.assertEqual(rsi.value, None) 101 | self.assertEqual(rsi.last, None) 102 | 103 | rsi.update(50.) # U = 0, D = 0 104 | self.assertEqual(rsi.value, 100.0) 105 | rsi.update(40.) # U = 0 D = 10 106 | self.assertEqual(rsi.value, 0.0) 107 | rsi.update(30.) # U = 0 D = 10 108 | self.assertEqual(rsi.value, 0.0) 109 | rsi.update(20.) # U = 0 D = 10 110 | self.assertEqual(rsi.value, 0.0) 111 | 112 | def test_Algorithm(self): 113 | rsi = RSI(9) 114 | self.assertEqual(rsi.value, None) 115 | self.assertEqual(rsi.last, None) 116 | 117 | rsi.update(50.) # U = 0, D = 0, ema_u = 0, ema_d = 0 118 | self.assertEqual(rsi.value, 100.0) 119 | rsi.update(40.) # U = 0 D = 10, ema_u = 0, ema_d = 2, rs = 0 120 | self.assertEqual(rsi.value, 0.0) 121 | rsi.update(50.) # U = 10 D = 0, ema_u = 2, ema_d = 1.6, rs = 1.25, rsi = 55.555555 122 | self.assertAlmostEqual(rsi.value, 55.555555555555) 123 | 124 | unittest.main() 125 | -------------------------------------------------------------------------------- /strategies/trending.py: -------------------------------------------------------------------------------- 1 | # A module for all built-in commands. 2 | # vim: sw=4: et 3 | LICENSE=""" 4 | Copyright (C) 2011 Michael Ihde 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | """ 20 | import datetime 21 | import os 22 | import tables 23 | import matplotlib.pyplot as plt 24 | import matplotlib.mlab as mlab 25 | import matplotlib.ticker as ticker 26 | 27 | from indicators.ema import EMA 28 | from indicators.rsi import RSI 29 | from indicators.simplevalue import SimpleValue 30 | from strategy import Strategy 31 | from utils.model import Order 32 | from utils.date import ONE_DAY 33 | 34 | class SymbolData(tables.IsDescription): 35 | date = tables.TimeCol() 36 | closing = tables.Float32Col() 37 | ema_short = tables.Float32Col() 38 | ema_long = tables.Float32Col() 39 | 40 | class Trending(Strategy): 41 | DEF_LONG_DAYS = 200 42 | DEF_SHORT_DAYS = 15 43 | DEF_RSI_PERIOD = 14 44 | 45 | def __init__(self, start_date, end_date, initial_position, market, params, h5file=None): 46 | Strategy.__init__(self, start_date, end_date, initial_position, market, params, h5file) 47 | for symbol in initial_position.keys(): 48 | if symbol == "$": 49 | continue 50 | 51 | self.addIndicator(symbol, "value", SimpleValue()) 52 | try: 53 | short = params['short'] 54 | except KeyError: 55 | short = Trending.DEF_SHORT_DAYS 56 | self.addIndicator(symbol, "short", EMA(short)) 57 | try: 58 | long_ = params['long'] 59 | except KeyError: 60 | long_ = Trending.DEF_LONG_DAYS 61 | self.addIndicator(symbol, "long", EMA(long_)) 62 | try: 63 | rsi = params['rsi'] 64 | except KeyError: 65 | rsi = Trending.DEF_RSI_PERIOD 66 | self.addIndicator(symbol, "rsi", RSI(rsi)) 67 | 68 | # Backfill the indicators 69 | try: 70 | backfill = params['backfill'] 71 | except KeyError: 72 | backfill = long_ 73 | 74 | d = start_date - (backfill * ONE_DAY) 75 | self.updateIndicators(d, start_date) 76 | 77 | def evaluate(self, date, position, market): 78 | self.updateIndicators(date) 79 | 80 | # Based of indicators, create signals 81 | buyTriggers = [] 82 | sellTriggers = [] 83 | for symbol, qty in position.items(): 84 | if symbol != '$': 85 | ticker = market[symbol] 86 | close_price = ticker[date].adjclose 87 | if self.indicators[symbol]["short"].value < self.indicators[symbol]["long"].value: 88 | sellTriggers.append(symbol) 89 | elif self.indicators[symbol]["short"].value > self.indicators[symbol]["long"].value: 90 | buyTriggers.append(symbol) 91 | 92 | # Using the basic MoneyManagement strategy, split all available cash 93 | # among all buy signals 94 | # Evaluate sell orders 95 | orders = [] 96 | for sellTrigger in sellTriggers: 97 | if position[sellTrigger].amount > 0: 98 | orders.append(Order(Order.SELL, sellTrigger, "ALL", Order.MARKET_PRICE)) 99 | 100 | # Evaluate all buy orders 101 | if len(buyTriggers) > 0: 102 | cash = position['$'] 103 | cashamt = position['$'] / len(buyTriggers) 104 | for buyTrigger in buyTriggers: 105 | ticker = market[buyTrigger] 106 | close_price = ticker[date].adjclose 107 | if close_price != None: 108 | estimated_shares = int(cashamt / close_price) 109 | # Only issues orders that buy at least one share 110 | if estimated_shares >= 1: 111 | orders.append(Order(Order.BUY, buyTrigger, "$%f" % cashamt, Order.MARKET_PRICE)) 112 | 113 | return orders 114 | 115 | CLAZZ = Trending 116 | -------------------------------------------------------------------------------- /quant: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | 20 | WARRANTY=""" 21 | BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE 22 | PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE 23 | STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE 24 | PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, 25 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 26 | FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 27 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU 28 | ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 29 | 30 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY 31 | COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE 32 | PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 33 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR 34 | INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA 35 | BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 36 | FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER 37 | OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 38 | """ 39 | import time 40 | import sys 41 | import os 42 | import traceback 43 | from pycommando.commando import command, Commando 44 | 45 | ############################################################################## 46 | # Some built-in commands 47 | @command("reload") 48 | def reload_(): 49 | """Quit""" 50 | mydir = os.path.abspath(os.path.dirname(sys.argv[0])) 51 | for cmd in os.listdir(mydir): 52 | if not cmd.endswith(".py"): 53 | continue 54 | name = cmd[0:-3] 55 | try: 56 | if name in sys.modules.keys(): 57 | reload(sys.modules[name]) 58 | else: 59 | __import__(name) 60 | except Exception: 61 | print "Error loading", name 62 | traceback.print_exc() 63 | 64 | @command("quit") 65 | @command("exit") 66 | def exit(): 67 | """Leave Quant""" 68 | sys.exit(0) 69 | 70 | @command("license") 71 | def license(): 72 | """Shows license information""" 73 | print LICENSE 74 | 75 | @command("warranty") 76 | def warranty(): 77 | """Shows warranty information""" 78 | print WARRANTY 79 | 80 | @command("wait") 81 | def wait(): 82 | """Block's until CTRL-C is pressed, only useful at the end of scripts""" 83 | print "Press CTRL-C to exit" 84 | while True: 85 | time.sleep(1) 86 | 87 | ############################################################################## 88 | # MAIN 89 | ############################################################################## 90 | if __name__ == "__main__": 91 | reload_() 92 | 93 | import readline 94 | import config 95 | import logging 96 | from optparse import OptionParser 97 | 98 | if not os.path.exists(config.QUANT_DIR): 99 | os.mkdir(config.QUANT_DIR) 100 | 101 | # Setup some basic logging 102 | logging.basicConfig() 103 | logging.getLogger().setLevel(logging.INFO) 104 | 105 | 106 | parser = OptionParser() 107 | parser.add_option("-i", "--iteractive", dest="interactive", default=False, action="store_true") 108 | (options, args) = parser.parse_args() 109 | 110 | # Right now we have no mode other than iteractive 111 | commando = Commando() 112 | if len(args) == 0 or options.interactive: 113 | if os.isatty(sys.stdin.fileno()): 114 | print "Welcome to Quant." 115 | print " Quant comes with ABSOLUTELY NO WARRANTY; type 'warranty' for details." 116 | print " This is free software, and you are welcome to redistribute it under certian conditions; type 'license' for details." 117 | print "Type 'help' to see a list of available commands." 118 | commando.cmdloop() 119 | else: 120 | commando.onecmd(" ".join(args)) 121 | -------------------------------------------------------------------------------- /utils/model.py: -------------------------------------------------------------------------------- 1 | #/usr/bin/env python 2 | # vim: sw=4: et 3 | LICENSE=""" 4 | Copyright (C) 2011 Michael Ihde 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | """ 20 | import logging 21 | import datetime 22 | import numpy 23 | import os 24 | import tables 25 | 26 | ############################################################################### 27 | # Data structures used by filters/strategies/riskmanagement 28 | ############################################################################### 29 | class Order(object): 30 | BUY = "BUY" 31 | SELL = "SELL" 32 | SHORT = "SHORT" 33 | COVER = "COVER" 34 | BUY_TO_OPEN = "BUY_TO_OPEN" 35 | BUY_TO_CLOSE = "BUY_TO_CLOSE" 36 | SELL_TO_CLOSE = "SELL_TO_CLOSE" 37 | SELL_TO_OPEN = "SELL_TO_OPEN" 38 | 39 | MARKET_PRICE = "MARKET_PRICE" 40 | MARKET_ON_CLOSE = "MARKET_ON_CLOSE" 41 | LIMIT = "LIMIT" 42 | STOP = "STOP" 43 | STOP_LIMIT = "STOP_LIMIT" 44 | 45 | def __init__(self, order, symbol, quantity, price_type, stop=None, limit=None): 46 | self._order = order 47 | self._sym = symbol 48 | self._qty = quantity 49 | self._price_type = price_type 50 | self._stop = stop 51 | self._limit = limit 52 | 53 | # Use propreties so that the Order class attributes behave as 54 | # readonly 55 | def getOrder(self): 56 | return self._order 57 | order = property(fget=getOrder) 58 | 59 | def getSymbol(self): 60 | return self._sym 61 | symbol = property(fget=getSymbol) 62 | 63 | def getQuantity(self): 64 | return self._qty 65 | quantity = property(fget=getQuantity) 66 | 67 | def getPriceType(self): 68 | return self._price_type 69 | price_type = property(fget=getPriceType) 70 | 71 | def getStop(self): 72 | return self._stop 73 | stop = property(fget=getStop) 74 | 75 | def getLimit(self): 76 | return self._limit 77 | limit = property(fget=getLimit) 78 | 79 | def __str__(self): 80 | if type(self._qty) == "float": 81 | qty = "$%0.2f" % self.quantity 82 | else: 83 | qty = self.quantity 84 | res = "%s %s %s at %s" % (self.order, qty, self.symbol, self.price_type) 85 | if self.price_type in (Order.LIMIT): 86 | res += " " + self.limit 87 | elif self.price_type in (Order.STOP): 88 | res += " " + self.stop 89 | elif self.price_type in (Order.STOP_LIMIT): 90 | res += " " + self.limit + " when " + self.stop 91 | return res 92 | 93 | class Position(object): 94 | def __init__(self, amount, basis): 95 | self.amount = amount 96 | self.basis = basis 97 | 98 | def add(self, qty, price_paid): 99 | v = (self.amount * self.basis) + (qty * price_paid) 100 | self.amount += qty 101 | self.basis = v / self.amount 102 | 103 | def remove(self, qty, price_sold): 104 | self.amount -= qty 105 | 106 | def __str__(self): 107 | return str(self.amount) 108 | 109 | ############################################################################### 110 | # PyTables data structures 111 | ############################################################################### 112 | class OrderData(tables.IsDescription): 113 | date = tables.TimeCol() 114 | order_type = tables.StringCol(16) 115 | symbol = tables.StringCol(16) 116 | date_str = tables.StringCol(16) 117 | order = tables.StringCol(64) 118 | executed_quantity = tables.Int32Col() 119 | executed_price = tables.Float32Col() 120 | basis = tables.Float32Col() 121 | 122 | class PositionData(tables.IsDescription): 123 | date = tables.TimeCol() 124 | date_str = tables.StringCol(16) 125 | symbol = tables.StringCol(16) 126 | amount = tables.Int32Col() 127 | value = tables.Float32Col() 128 | basis = tables.Float32Col() # The basis using single-category averaging 129 | 130 | class PerformanceData(tables.IsDescription): 131 | date = tables.TimeCol() 132 | date_str = tables.StringCol(16) 133 | value = tables.Float32Col() 134 | 135 | ############################################################################### 136 | # Helper functions 137 | ############################################################################### 138 | def openOutputFile(filepath): 139 | """After opening the file, get the tables like this. 140 | 141 | file.getNode("/Orders") 142 | file.getNode("/Position") 143 | file.getNode("/Performance") 144 | """ 145 | try: 146 | os.remove(os.path.expanduser(filepath)) 147 | except OSError: 148 | pass 149 | outputFile = tables.openFile(os.path.expanduser(filepath), mode="w", title="Quant Simulation") 150 | outputFile.createTable("/", 'Orders', OrderData) 151 | outputFile.createTable("/", 'Position', PositionData) 152 | outputFile.createTable("/", 'Performance', PositionData) 153 | return outputFile 154 | -------------------------------------------------------------------------------- /report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | LICENSE=""" 3 | Copyright (C) 2011 Michael Ihde 4 | 5 | This program is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU General Public License 7 | as published by the Free Software Foundation; either version 2 8 | of the License, or (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | """ 19 | import os 20 | import tables 21 | import datetime 22 | import math 23 | from utils.progress_bar import ProgressBar 24 | from pycommando.commando import command 25 | 26 | def calculate_performance(inputfname="~/.quant/simulation.h5"): 27 | report = {} 28 | 29 | inputFile = tables.openFile(os.path.expanduser(inputfname), "r") 30 | try: 31 | tbl = inputFile.getNode("/Performance", classname="Table") 32 | 33 | equity_curve = zip([datetime.datetime.fromordinal(int(x)) for x in tbl.col("date")], tbl.col("value")) 34 | starting_date, starting_value = equity_curve[0] 35 | ending_date, ending_value = equity_curve[-1] 36 | 37 | # Analysis 38 | max_draw_down_duration = {'days': 0, 'start': None, 'end': None} 39 | max_draw_down_amount = {'amount': 0.0, 'high': None, 'low': None} 40 | daily_returns = [10000.0] 41 | #benchmark_returns = [10000.0] 42 | #excess_returns = [] 43 | 44 | last = equity_curve[0] 45 | highwater = equity_curve[0] # The highwater date and equity 46 | lowwater = equity_curve[0] # The highwater date and equity 47 | for date, equity in equity_curve[1:]: 48 | # If we have passed the highwater or we are at the end of the simulation 49 | if equity >= highwater[1] or date == equity_curve[-1][0]: 50 | drawdown_dur = (date - highwater[0]).days 51 | drawdown_amt = highwater[1] - lowwater[1] 52 | if drawdown_dur > max_draw_down_duration['days']: 53 | max_draw_down_duration['days'] = drawdown_dur 54 | max_draw_down_duration['start'] = highwater 55 | max_draw_down_duration['end'] = (date, equity) 56 | if drawdown_amt > max_draw_down_amount['amount']: 57 | max_draw_down_amount['amount'] = drawdown_amt 58 | max_draw_down_amount['high'] = highwater 59 | max_draw_down_amount['low'] = lowwater 60 | highwater = (date, equity) 61 | lowwater = (date, equity) 62 | 63 | if equity <= lowwater[1]: 64 | lowwater = (date, equity) 65 | 66 | daily_return = (equity - last[1]) / last[1] 67 | daily_returns.append((daily_return * daily_returns[-1]) + daily_returns[-1]) 68 | 69 | last = (date, equity) 70 | 71 | total_days = (ending_date - starting_date).days 72 | total_years = float(total_days) / 365.0 73 | equity_return = ending_value - starting_value 74 | equity_percent = 100.0 * (equity_return / starting_value) 75 | cagr = 100.0 * (math.pow((ending_value / starting_value), (1 / total_years)) - 1) 76 | drawdown_percent = 0.0 77 | if max_draw_down_amount['high'] != None: 78 | drawdown_percent = 100.0 * (max_draw_down_amount['amount'] / max_draw_down_amount['high'][1]) 79 | 80 | report['period'] = total_days 81 | report['starting_date'] = starting_date 82 | report['starting_value'] = starting_value 83 | report['ending_date'] = ending_date 84 | report['ending_value'] = ending_value 85 | report['ending_value'] = ending_value 86 | report['equity_return'] = equity_return 87 | report['equity_percent'] = equity_percent 88 | report['cagr'] = cagr 89 | report['drawdown_dur'] = max_draw_down_duration['days'] 90 | report['drawdown_amt'] = max_draw_down_amount['amount'] 91 | report['drawdown_per'] = drawdown_percent 92 | report['initial_pos'] = (starting_date, starting_value) 93 | report['final_pos'] = (ending_date, ending_value) 94 | 95 | report['orders'] = [] 96 | # Calculate cost-basis/and profit on trades using single-category method 97 | tbl = inputFile.getNode("/Orders", classname="Table") 98 | winning = 0 99 | losing = 0 100 | largest_winning = (0, "") 101 | largest_losing = (0, "") 102 | conseq_win = 0 103 | conseq_lose = 0 104 | largest_conseq_win = 0 105 | largest_conseq_lose = 0 106 | total_profit = 0 107 | total_win = 0 108 | total_lose = 0 109 | total_trades = 0 110 | for order in tbl.iterrows(): 111 | o = (datetime.datetime.fromordinal(order['date']).date(), order['executed_quantity'], order['executed_price'], order['basis'], order['order']) 112 | report['orders'].append(o) 113 | if order['order_type'] == "SELL": 114 | total_trades += 1 115 | profit = (order['executed_price'] - order['basis']) * order['executed_quantity'] 116 | total_profit += profit 117 | if profit > 0: 118 | winning += 1 119 | conseq_win += 1 120 | total_win += profit 121 | if conseq_lose > largest_conseq_lose: 122 | largest_conseq_lose = conseq_lose 123 | conseq_lose = 0 124 | elif profit < 0: 125 | losing += 1 126 | conseq_lose += 1 127 | total_lose += profit 128 | if conseq_win > largest_conseq_win: 129 | largest_conseq_win = conseq_win 130 | conseq_win = 0 131 | 132 | if profit > largest_winning[0]: 133 | largest_winning = (profit, order['date_str']) 134 | if profit < largest_losing[0]: 135 | largest_losing = (profit, order['date_str']) 136 | 137 | if winning == 0: 138 | avg_winning = 0 139 | else: 140 | avg_winning = total_win / winning 141 | if losing == 0: 142 | avg_losing = 0 143 | else: 144 | avg_losing = total_lose / losing 145 | if conseq_win > largest_conseq_win: 146 | largest_conseq_win = conseq_win 147 | if conseq_lose > largest_conseq_lose: 148 | largest_conseq_lose = conseq_lose 149 | 150 | report['total_trades'] = total_trades 151 | report['winning_trades'] = winning 152 | report['losing_trades'] = losing 153 | if total_trades > 0: 154 | report['avg_trade'] = total_profit / total_trades 155 | else: 156 | report['avg_trade'] = 0 157 | report['avg_winning_trade'] = avg_winning 158 | report['avg_losing_trade'] = avg_losing 159 | report['conseq_win'] = largest_conseq_win 160 | report['conseq_lose'] = largest_conseq_lose 161 | report['largest_win'] = largest_winning 162 | report['largest_lose'] = largest_losing 163 | 164 | finally: 165 | inputFile.close() 166 | 167 | return report 168 | 169 | @command("report_performance") 170 | def report_performance(inputfname="~/.quant/simulation.h5"): 171 | report = calculate_performance(inputfname) 172 | print 173 | print "######################################################################################" 174 | print " Report:", inputfname 175 | print 176 | print "Simulation Period: %(period)s days" % report 177 | print "Starting Value: $%(starting_value)0.2f" % report 178 | print "Ending Value: $%(ending_value)0.2f" % report 179 | print "Return: $%(equity_return)0.2f (%(equity_percent)3.2f%%)" % report 180 | print "CAGR: %(cagr)3.2f%%" % report 181 | print "Maximum Drawdown Duration: %(drawdown_dur)d days" % report 182 | print "Maxium Drawdown Amount: $%(drawdown_amt)0.2f (%(drawdown_per)3.2f%%)" % report 183 | print "Inital Position:", report['starting_date'], report['starting_value'] 184 | print "Final Position:", report['ending_date'], report['ending_value'] 185 | print 186 | 187 | # Calculate cost-basis/and profit on trades using single-category method 188 | print "Date\t\tQty\tPrice\tBasis\tOrder" 189 | for order in report['orders']: 190 | print "%s\t%0.2f\t%0.2f\t%0.2f\t%s" % order 191 | 192 | if report['total_trades'] > 0: 193 | print 194 | print "Total # of Trades:", report['total_trades'] 195 | print "# of Winning Trades:", report['winning_trades'] 196 | print "# of Losing Trades:", report['losing_trades'] 197 | print "Percent Profitable:", report['winning_trades'] / report['total_trades'] 198 | print 199 | print "Average Trade:", report['avg_trade'] 200 | print "Average Winning Trade:", report['avg_winning_trade'] 201 | print "Average Losing Trade:", report['avg_losing_trade'] 202 | print 203 | print "Max. conseq. Winners:", report['conseq_win'] 204 | print "Max. conseq. Losers:", report['conseq_lose'] 205 | print "Largest Winning Trade:", report['largest_win'][0], report['largest_win'][1] 206 | print "Largest Losing Trade:", report['largest_lose'][0], report['largest_lose'][1] 207 | print "######################################################################################" 208 | print 209 | 210 | @command("list_orders") 211 | def list_orders(input_="~/.quant/simulation.h5", node="/Orders"): 212 | try: 213 | inputFile = tables.openFile(os.path.expanduser(input_), "r") 214 | tbl = inputFile.getNode(node, classname="Table") 215 | for d in tbl.iterrows(): 216 | print d 217 | finally: 218 | inputFile.close() 219 | -------------------------------------------------------------------------------- /pycommando/commando.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: sw=4: et: 3 | # 4 | # Copyright (c) 2010 by Michael Ihde 5 | # 6 | # All Rights Reserved 7 | # 8 | # Permission to use, copy, modify, and distribute this software 9 | # and its documentation for any purpose and without fee is hereby 10 | # granted, provided that the above copyright notice appear in all 11 | # copies and that both that copyright notice and this permission 12 | # notice appear in supporting documentation, and that the name of 13 | # Michael Ihde not be used in advertising or publicity 14 | # pertaining to distribution of the software without specific, written 15 | # prior permission. 16 | # 17 | # Michael Ihde DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS 18 | # SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 19 | # AND FITNESS, IN NO EVENT SHALL Michael Ihde BE LIABLE FOR 20 | # ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 21 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 22 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 23 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 24 | # PERFORMANCE OF THIS SOFTWARE. 25 | # 26 | import inspect 27 | import sys 28 | import string 29 | import cmd 30 | import new 31 | import traceback 32 | import pprint 33 | import os 34 | 35 | class Commando(cmd.Cmd): 36 | 37 | ISATTY = True 38 | def __init__(self, completekey='tab', stdin=sys.stdin, stdout=sys.stdout): 39 | cmd.Cmd.__init__(self, completekey, stdin, stdout) 40 | Commando.ISATTY = os.isatty(stdin.fileno()) 41 | if not Commando.ISATTY: 42 | self.prompt = "" 43 | 44 | def do_shell(self, argstr): 45 | pass 46 | 47 | def precmd(self, cmd): 48 | if cmd == "EOF": 49 | raise SystemExit 50 | return cmd 51 | 52 | def emptyline(self): 53 | pass 54 | 55 | def cmdloop(self, intro=None): 56 | try: 57 | cmd.Cmd.cmdloop(self, intro) 58 | except KeyboardInterrupt: 59 | pass 60 | except SystemExit: 61 | pass 62 | print 63 | 64 | def parseargs(argstr): 65 | """Args are separated by white-space or commas. Unless a value is 66 | surrounded by single quotes, white space will be trimmed. 67 | 68 | >>> parseargs('A B C') 69 | ('A', 'B', 'C') 70 | 71 | >>> parseargs('A B C') 72 | ('A', 'B', 'C') 73 | 74 | >>> parseargs('A, B, C') 75 | ('A', 'B', 'C') 76 | 77 | >>> parseargs('A B, C') 78 | ('A', 'B', 'C') 79 | 80 | >>> parseargs('A, B, C') 81 | ('A', 'B', 'C') 82 | 83 | >>> parseargs('A, B C') 84 | ('A', 'B', 'C') 85 | 86 | >>> parseargs('A ,, C') 87 | ('A', None, 'C') 88 | 89 | >>> parseargs("'A ' ' B ' C") 90 | ('A ', ' B ', 'C') 91 | 92 | >>> parseargs("'A, B, C'") 93 | ('A, B, C',) 94 | 95 | >>> parseargs("'A, B' C") 96 | ('A, B', 'C') 97 | """ 98 | args = [] 99 | def parser(): 100 | while True: 101 | char = (yield) 102 | if char != ' ': 103 | arg_accumulator = [] 104 | if char not in (',', "'", " "): 105 | arg_accumulator.append(char) 106 | if char == "'": 107 | while True: 108 | char = (yield) 109 | if char == "'": 110 | break 111 | else: 112 | arg_accumulator.append(char) 113 | while True: 114 | char = (yield) 115 | if char in (',', " ", None): 116 | arg = "".join(arg_accumulator) 117 | if arg == "": 118 | args.append(None) 119 | else: 120 | args.append(arg) 121 | break 122 | else: 123 | arg_accumulator.append(char) 124 | 125 | p = parser() 126 | p.send(None) # Start up the coroutine 127 | for char in argstr: 128 | p.send(char) 129 | p.send(None) 130 | 131 | return tuple(args) 132 | 133 | # DECORATOR 134 | class command(object): 135 | def __init__(self, name, prompts=(), category=None): 136 | self.name = name 137 | self.prompts = {} 138 | for argname, prompt, argtype in prompts: 139 | self.prompts[argname] = (prompt, argtype) 140 | 141 | def promptForYesNo(self, prompt, default): 142 | val = None 143 | if not Commando.ISATTY: 144 | val = default 145 | else: 146 | while val == None: 147 | if default == None: 148 | input = raw_input(prompt + " Y/N: ") 149 | if input.upper() in ("Y", "YES"): 150 | val = True 151 | elif input.upper() in ("N", "NO"): 152 | val = False 153 | else: 154 | if default == True: 155 | val = raw_input(prompt + " [Y]/N: ") 156 | elif default == False: 157 | val = raw_input(prompt + " Y/[N]: ") 158 | else: 159 | raise ValueError 160 | 161 | if val.strip() == "": 162 | val = default 163 | elif val.upper() in ("Y", "YES"): 164 | val = True 165 | elif val.upper() in ("N", "NO"): 166 | val = False 167 | 168 | return val 169 | 170 | def promptForValue(self, prompt, default, val_type): 171 | val = None 172 | if not Commando.ISATTY: 173 | val = default 174 | else: 175 | while val == None: 176 | if default == None: 177 | input = raw_input(prompt + ": ") 178 | if input.strip() != "": 179 | val = input 180 | else: 181 | val = raw_input(prompt + " [%s]: " % (default)) 182 | if val.strip() == "": 183 | val = default 184 | try: 185 | val = val_type(val) 186 | except ValueError: 187 | val = None 188 | 189 | return val 190 | 191 | def __call__(self, f): 192 | # Pull out meta data about the function 193 | f_args, f_varargs, f_varkwargs, f_defaults = inspect.getargspec(f) 194 | if f_defaults != None: 195 | f_first_default_index = len(f_args) - len(f_defaults) 196 | else: 197 | f_first_default_index = None 198 | 199 | # Define the wrapped function 200 | def wrapped_f(commando, argstr): 201 | args = parseargs(argstr) 202 | vals = [] 203 | 204 | for i in xrange(len(f_args)): 205 | # See if this argument has a default or not 206 | default = None 207 | if f_first_default_index != None and i >= f_first_default_index: 208 | default = f_defaults[i - f_first_default_index] 209 | 210 | try: 211 | text, val_type = self.prompts[f_args[i]] 212 | except KeyError: 213 | # No prompt was provided, so use a generic one 214 | text = "Enter %s" % (f_args[i]) 215 | # infer the type from the default when possible 216 | if default != None: 217 | val_type = type(default) 218 | else: 219 | val_type = str 220 | 221 | val = None 222 | if i < len(args): 223 | # The user passed the value so we don't need to prompt 224 | # if args[i] is None (not to be confused with "None") 225 | # then they explictly wanted the default (without a prompt) 226 | # because they entered two commas back to back with an 227 | # empty string 228 | if (args[i]) != None: 229 | if val_type == bool: 230 | if args[i].upper() in ("Y", "YES", "TRUE"): 231 | val = True 232 | elif args[i].upper() in ("N", "NO", "FALSE"): 233 | val = False 234 | else: 235 | raise ValueError 236 | else: 237 | val = val_type(args[i]) 238 | elif (args[i]) == None and default != None: 239 | val = default 240 | else: 241 | if val_type == bool: 242 | val = self.promptForYesNo(text, default) 243 | else: 244 | val = self.promptForValue(text, default, val_type) 245 | else: 246 | # Treat bools as yes/no 247 | if val_type == bool: 248 | val = self.promptForYesNo(text, default) 249 | else: 250 | val = self.promptForValue(text, default, val_type) 251 | vals.append(val) 252 | 253 | if f_varargs != None and len(args) > len(f_args): 254 | vals.extend(args[len(f_args):]) 255 | # Call the function and print the result using pprint 256 | try: 257 | result = f(*vals) 258 | if result != None: 259 | if type(result) in (dict, list, tuple): 260 | pprint.pprint(result) 261 | else: 262 | print result 263 | except Exception, e: 264 | traceback.print_exc() 265 | if not Commando.ISATTY: 266 | raise SystemExit 267 | 268 | # Inherit the provided docstring 269 | # and augment it with information about the arguments 270 | wrapped_f.__doc__ = f.__doc__ 271 | wrapped_f.__doc__ = "\nUsage: %s %s\n\n %s" % (self.name, " ".join(f_args), f.__doc__) 272 | 273 | f_name = "do_" + self.name 274 | setattr(Commando, f_name, new.instancemethod(wrapped_f, None, Commando)) 275 | 276 | # Don't return the wrapped function, because we want 277 | # to be able to call the functions without the prompt logic 278 | return f 279 | 280 | if __name__ == "__main__": 281 | import doctest 282 | doctest.testmod() 283 | -------------------------------------------------------------------------------- /simulation.py: -------------------------------------------------------------------------------- 1 | #/usr/bin/env python 2 | # vim: sw=4: et 3 | LICENSE=""" 4 | Copyright (C) 2011 Michael Ihde 5 | 6 | This program is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU General Public License 8 | as published by the Free Software Foundation; either version 2 9 | of the License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | """ 20 | import logging 21 | import datetime 22 | import numpy 23 | import os 24 | import sys 25 | import tables 26 | import math 27 | import yaml 28 | from config import CONFIG 29 | from yahoo import Market 30 | from utils.progress_bar import ProgressBar 31 | from utils.model import * 32 | from utils.market import * 33 | from utils.date import ONE_DAY 34 | from pycommando.commando import command 35 | import matplotlib.pyplot as plt 36 | import matplotlib.mlab as mlab 37 | import matplotlib.ticker as ticker 38 | import matplotlib.dates as dates 39 | 40 | MARKET = Market() 41 | 42 | def initialize_position(portfolio, date): 43 | p = CONFIG['portfolios'][portfolio] 44 | 45 | if not type(date) == datetime.datetime: 46 | date = datetime.datetime.strptime(date, "%Y-%m-%d") 47 | 48 | # Turn the initial cash value into shares based off the portfolio percentage 49 | position = {'$': 0.0} 50 | market = Market() 51 | for instrument, amt in p.items(): 52 | instrument = instrument.strip() 53 | if type(amt) == str: 54 | amt = amt.strip() 55 | 56 | if instrument == "$": 57 | position[instrument] += float(amt) 58 | else: 59 | d = date 60 | price = market[instrument][d].adjclose 61 | while price == None: 62 | # Walk backwards looking for a day that had a close price, but not too far 63 | # because the given instrument may not exist at any time for the given 64 | # date or prior to it 65 | d = d - ONE_DAY 66 | if (date - d) > datetime.timedelta(days=7): 67 | break 68 | price = market[instrument][d].adjclose 69 | if price == None: 70 | # This occurs it the instrument does not exist in the market 71 | # at the start of the simulation period 72 | position[instrument] = Position(0.0, 0.0) 73 | if type(amt) == str and amt.startswith('$'): 74 | amt = float(amt[1:]) 75 | position['$'] += amt 76 | else: 77 | print "Warning. Non-cash value used for instrument that is not available at start of simulation period" 78 | else: 79 | if type(amt) == str and amt.startswith('$'): 80 | amt = float(amt[1:]) 81 | amt = math.floor(amt / price) 82 | position[instrument] = Position(float(amt), price) 83 | return position 84 | 85 | def write_position(table, position, date): 86 | for instrument, p in position.items(): 87 | table.row['date'] = date.date().toordinal() 88 | table.row['date_str'] = str(date.date()) 89 | table.row['symbol'] = instrument 90 | if instrument == '$': 91 | table.row['amount'] = 0 92 | table.row['value'] = p 93 | else: 94 | table.row['amount'] = p.amount 95 | table.row['basis'] = p.basis 96 | price = MARKET[instrument][date].adjclose 97 | if price: 98 | table.row['value'] = price 99 | else: 100 | table.row['value'] = 0.0 101 | table.row.append() 102 | 103 | def write_performance(table, position, date): 104 | value = 0.0 105 | for instrument, p in position.items(): 106 | if instrument == '$': 107 | value += p 108 | else: 109 | price = MARKET[instrument][date].adjclose 110 | if price: 111 | value += (price * p.amount) 112 | 113 | table.row['date'] = date.date().toordinal() 114 | table.row['date_str'] = str(date.date()) 115 | table.row['value'] = value 116 | table.row.append() 117 | 118 | def execute_orders(table, position, date, orders): 119 | for order in orders: 120 | logging.debug("Executing order %s", order) 121 | if position.has_key(order.symbol): 122 | ticker = MARKET[order.symbol] 123 | if order.order == Order.SELL: 124 | if order.price_type == Order.MARKET_PRICE: 125 | strike_price = ticker[date].adjopen 126 | elif order.price_type == Order.MARKET_ON_CLOSE: 127 | strike_price = ticker[date].adjclose 128 | else: 129 | raise StandardError, "Unsupport price type" 130 | 131 | qty = None 132 | if order.quantity == "ALL": 133 | qty = position[order.symbol].amount 134 | else: 135 | qty = order.quantity 136 | 137 | if qty > position[order.symbol] or qty < 1: 138 | logging.warn("Ignoring invalid order %s. Invalid quantity", order) 139 | continue 140 | 141 | price_paid = 0.0 142 | 143 | table.row['date'] = date.date().toordinal() 144 | table.row['date_str'] = str(date.date()) 145 | table.row['order_type'] = order.order 146 | table.row['symbol'] = order.symbol 147 | table.row['order'] = str(order) 148 | table.row['executed_quantity'] = qty 149 | table.row['executed_price'] = strike_price 150 | table.row['basis'] = position[order.symbol].basis 151 | table.row.append() 152 | 153 | position[order.symbol].remove(qty, strike_price) 154 | position['$'] += (qty * strike_price) 155 | position['$'] -= 9.99 # TODO make trading cost configurable 156 | 157 | elif order.order == Order.BUY: 158 | if order.price_type == Order.MARKET_PRICE: 159 | strike_price = ticker[date].adjopen 160 | elif order.price_type == Order.MARKET_ON_CLOSE: 161 | strike_price = ticker[date].adjclose 162 | 163 | if type(order.quantity) == str and order.quantity[0] == "$": 164 | qty = int(float(order.quantity[1:]) / strike_price) 165 | else: 166 | qty = int(order.quantity) 167 | 168 | table.row['date'] = date.date().toordinal() 169 | table.row['date_str'] = str(date.date()) 170 | table.row['order_type'] = order.order 171 | table.row['symbol'] = order.symbol 172 | table.row['order'] = str(order) 173 | table.row['executed_quantity'] = qty 174 | table.row['executed_price'] = strike_price 175 | table.row['basis'] = 0.0 176 | table.row.append() 177 | 178 | position[order.symbol].add(qty, strike_price) 179 | position['$'] -= (qty * strike_price) 180 | position['$'] -= 9.99 181 | 182 | 183 | def load_strategy(name): 184 | mydir = os.path.abspath(os.path.dirname(sys.argv[0])) 185 | strategydir = os.path.join(mydir, "strategies") 186 | sys.path.insert(0, strategydir) 187 | if name in sys.modules.keys(): 188 | reload(sys.modules[name]) 189 | else: 190 | __import__(name) 191 | 192 | clazz = getattr(sys.modules[name], "CLAZZ") 193 | sys.path.pop(0) 194 | 195 | return clazz 196 | 197 | 198 | @command("analyze") 199 | def analyze(strategy_name, portfolio, strategy_params="{}"): 200 | """Using a given strategy and portfolio, make a trading decision""" 201 | now = datetime.datetime.today() 202 | position = initialize_position(portfolio, now) 203 | 204 | # Initialize the strategy 205 | params = yaml.load(strategy_params) 206 | strategy_clazz = load_strategy(strategy_name) 207 | strategy = strategy_clazz(now, now, position, MARKET, params) 208 | 209 | orders = strategy.evaluate(now, position, MARKET) 210 | 211 | for order in orders: 212 | print order 213 | 214 | @command("simulate") 215 | def simulate(strategy_name, portfolio, start_date, end_date, output="~/.quant/simulation.h5", strategy_params="{}"): 216 | """A simple simulator that simulates a strategy that only makes 217 | decisions at closing. Only BUY and SELL orders are supported. Orders 218 | are only good for the next day. 219 | 220 | A price type of MARKET is executed at the open price the next day. 221 | 222 | A price type of MARKET_ON_CLOSE is executed at the close price the next day. 223 | 224 | A price type of LIMIT will be executed at the LIMIT price the next day if LIMIT 225 | is between the low and high prices of the day. 226 | 227 | A price type of STOP will be executed at the STOP price the next day if STOP 228 | is between the low and high prices of the day. 229 | 230 | A price type of STOP_LIMIT will be executed at the LIMIT price the next day if STOP 231 | is between the low and high prices of the day. 232 | """ 233 | 234 | outputFile = openOutputFile(output) 235 | # Get some of the tables from the output file 236 | order_tbl = outputFile.getNode("/Orders") 237 | postion_tbl = outputFile.getNode("/Position") 238 | performance_tbl = outputFile.getNode("/Performance") 239 | 240 | start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d") 241 | end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d") 242 | # Start the simulation at closing of the previous trading day 243 | now = getPrevTradingDay(start_date) 244 | 245 | try: 246 | position = initialize_position(portfolio, now) 247 | 248 | # Pre-cache some info to make the simulation faster 249 | ticker = MARKET["^DJI"].updateHistory(start_date, end_date) 250 | for symbol in position.keys(): 251 | if symbol != '$': 252 | MARKET[symbol].updateHistory(start=start_date, end=end_date) 253 | days = (end_date - start_date).days 254 | 255 | # Initialize the strategy 256 | params = yaml.load(strategy_params) 257 | strategy_clazz = load_strategy(strategy_name) 258 | strategy = strategy_clazz(start_date, end_date, position, MARKET, params, outputFile) 259 | 260 | p = ProgressBar(maxValue=days, totalWidth=80) 261 | print "Starting Simulation" 262 | 263 | while now <= end_date: 264 | 265 | # Write the initial position to the database 266 | write_position(postion_tbl, position, now) 267 | write_performance(performance_tbl, position, now) 268 | 269 | # Remember 'now' is after closing, so the strategy 270 | # can use any information from 'now' or earlier 271 | orders = strategy.evaluate(now, position, MARKET) 272 | 273 | # Go to the next day to evalute the orders 274 | now += ONE_DAY 275 | while not isTradingDay(now): 276 | now += ONE_DAY 277 | p.performWork(1) 278 | continue 279 | 280 | # Execute orders 281 | execute_orders(order_tbl, position, now, orders) 282 | 283 | # Flush the data to disk 284 | outputFile.flush() 285 | p.performWork(1) 286 | print p, '\r', 287 | 288 | p.updateAmount(p.max) 289 | print p, '\r', 290 | print '\n' # End the progress bar here before calling finalize 291 | orders = strategy.finalize() 292 | finally: 293 | outputFile.close() 294 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /utils/YahooQuote.py: -------------------------------------------------------------------------------- 1 | LICENSE=""" 2 | This module is released under the GNU Lesser General Public License, 3 | the wording of which is available on the GNU website, http://www.gnu.org 4 | """ 5 | 6 | """ 7 | YahooQuote - a set of classes for fetching stock quotes from 8 | finance.yahoo.com 9 | 10 | Created by David McNab ((david AT conscious DOT co DOT nz)) 11 | 12 | Originally this was a wrapper of the original 13 | 'pyq' utility by Rimon Barr: 14 | - http://www.cs.cornell.edu/barr/repository/pyq/index.html 15 | But now, the back-end has been completely re-written. 16 | 17 | The original version stored stock quotes in a MetaKit database 18 | file. Since metakit isn't available as a Ubuntu package, the 19 | cache storage has been changed to sqlite3 to be as portable 20 | as possible. 21 | 22 | Usage Examples:: 23 | 24 | >>> import YahooQuote 25 | 26 | >>> market = YahooQuote.Market() 27 | 28 | >>> redhat = market['rht'] 29 | >>> redhat 30 | 31 | 32 | >>> redhat[20070601] 33 | 34 | 35 | >>> redhat.now # magic attribute via __getattr__ 36 | 37 | 38 | >>> ms = market['msft'] 39 | >>> ms 40 | # note the 'dji/msft', meaning that MS is on DJI index 41 | 42 | >>> ms[20070604:20070609] # get range of dates as slice - Jun4 - Jun8 43 | [, 44 | , 45 | , 46 | ] 47 | 48 | >>> lastThu = ms[20070614] # fetch a single trading day 49 | 50 | >>> lastThu 51 | 52 | 53 | >>> lastThu.__dict__ # see what is in a Quote object 54 | {'volume': 59065700, 'open': 30.350000000000001, 'high': 30.710000000000001, 55 | 'adjclose': 30.52, 'low': 30.300000000000001, 'date': '20070614', 56 | 'close': 30.52, 'ticker': } 57 | 58 | >>> ms[20070603] # sunday, markets closed, build dummy quote from Friday 59 | 60 | 61 | Read the API documentation for further features/methods 62 | """ 63 | 64 | import os, sys, re, traceback, getopt, time 65 | #import strptime 66 | import urllib 67 | import weakref, gc 68 | import csv 69 | import logging 70 | import Queue, thread, threading 71 | import datetime 72 | import sqlite3 73 | 74 | Y2KCUTOFF=60 75 | __version__ = "0.4" 76 | 77 | CACHE='~/.quant/stocks.db' 78 | 79 | MONTH2NUM = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 80 | 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12} 81 | 82 | DAYSECS = 60 * 60 * 24 83 | 84 | DEBUG = 0 85 | 86 | ENABLE_INTERPOLATION = False 87 | 88 | # base URLs for fetching quotes and history 89 | baseUrlHistory = "http://ichart.finance.yahoo.com/table.csv" 90 | baseUrlQuote = "http://download.finance.yahoo.com/d/quotes.csv" 91 | 92 | fetchWindow = 90 93 | 94 | # maximum number of threads for fetching histories 95 | maxHistoryThreads = 10 96 | 97 | # hardwired table listing the various stock indexes around the world. 98 | # from time to time, this will need to be updated 99 | indexes = { 100 | "DJA": {"name": "Dow Jones Composite", "country": "USA"}, 101 | "DJI": {"name": "Dow Jones Industrial Average", "country": "USA"}, 102 | "DJT": {"name": "Dow Jones Transportation Average", "country": "USA"}, 103 | "DJU": {"name": "Dow Jones Utility Average", "country": "USA"}, 104 | "NYA": {"name": "NYSE Composite", "country": "USA"}, 105 | "NIN": {"name": "NYSE International 100", "country": "USA"}, 106 | "NTM": {"name": "NYSE TMT", "country": "USA"}, 107 | "NUS": {"name": "NYSE US 100", "country": "USA"}, 108 | "NWL": {"name": "NYSE World Leaders", "country": "USA"}, 109 | "IXBK":{"name": "NASDAQ Bank", "country": "USA"}, 110 | "NBI": {"name": "NASDAQ Biotech", "country": "USA"}, 111 | "IXIC":{"name": "NASDAQ Composite", "country": "USA"}, 112 | "IXK": {"name": "NASDAQ Computer", "country": "USA"}, 113 | "IXF": {"name": "NASDAQ Financial 100", "country": "USA"}, 114 | "IXID":{"name": "NASDAQ Industrial", "country": "USA"}, 115 | "IXIS":{"name": "NASDAQ Insurance", "country": "USA"}, 116 | "IXQ": {"name": "NASDAQ NNM Composite", "country": "USA"}, 117 | "IXFN":{"name": "NASDAQ Other Finance", "country": "USA"}, 118 | "IXUT":{"name": "NASDAQ Telecommunications", "country": "USA"}, 119 | "IXTR":{"name": "NASDAQ Transportation", "country": "USA"}, 120 | "NDX": {"name": "NASDAQ-100 (DRM)", "country": "USA"}, 121 | "OEX": {"name": "S&P 100 Index", "country": "USA"}, 122 | "MID": {"name": "S&P 400 Midcap Index", "country": "USA"}, 123 | "GSPC":{"name": "S&P 500 Index", "country": "USA"}, 124 | "SPSUPX":{"name": "S&P Composite 1500 Index", "country": "USA"}, 125 | "SML": {"name": "S&P Smallcap 600 Index", "country": "USA"}, 126 | "XAX": {"name": "AMEX COMPOSITE INDEX", "country": "USA"}, 127 | "IIX": {"name": "AMEX INTERACTIVE WEEK INTERNET", "country": "USA"}, 128 | "NWX": {"name": "AMEX NETWORKING INDEX", "country": "USA"}, 129 | "PSE": {"name": "ArcaEx Tech 100 Index", "country": "USA"}, 130 | "DWC": {"name": "DJ WILSHIRE 5000", "country": "USA"}, 131 | "XMI": {"name": "MAJOR MARKET INDEX", "country": "USA"}, 132 | "SOXX": {"name": "PHLX SEMICONDUCTOR SECTOR INDEX", "country": "USA"}, 133 | "DOT": {"name": "PHLX THESTREET.COM INTERNET SEC", "country": "USA"}, 134 | "RUI": {"name": "RUSSELL 1000 INDEX", "country": "USA"}, 135 | "RUT": {"name": "RUSSELL 2000 INDEX", "country": "USA"}, 136 | "RUA": {"name": "RUSSELL 3000 INDEX", "country": "USA"}, 137 | "MERV": {"name": "MerVal", "country": "?"}, 138 | "BVSP": {"name": "Bovespa", "country": "?"}, 139 | "GSPTSE": {"name": "S&P TSX Composite", "country": "?"}, 140 | "MXX": {"name": "IPC", "country": "?"}, 141 | "GSPC": {"name": "500 Index", "country": "?"}, 142 | "AORD": {"name": "All Ordinaries", "country": "Australia"}, 143 | "SSEC": {"name": "Shanghai Composite", "country": "China"}, 144 | "HSI": {"name": "Hang Seng", "country": "Hong Kong"}, 145 | "BSESN": {"name": "BSE", "country": "?"}, 146 | "JKSE": {"name": "Jakarta Composite", "country": "Indonesia"}, 147 | "KLSE": {"name": "KLSE Composite", "country": "?"}, 148 | "N225": {"name": "Nikkei 225", "country": "Japan"}, 149 | "NZ50": {"name": "NZSE 50", "country": "New Zealand"}, 150 | "STI": {"name": "Straits Times", "country": "?"}, 151 | "KS11": {"name": "Seoul Composite", "country": "?"}, 152 | "TWII": {"name": "Taiwan Weighted", "country": "Taiwan"}, 153 | "ATX": {"name": "ATX", "country": "?"}, 154 | "BFX": {"name": "BEL-20", "country": "?"}, 155 | "FCHI": {"name": "CAC 40", "country": "?"}, 156 | "GDAXI": {"name": "DAX", "country": "?"}, 157 | "AEX": {"name": "AEX General", "country": "?"}, 158 | "OSEAX": {"name": "OSE All Share", "country": "?"}, 159 | "MIBTEL": {"name": "MIBTel", "country": "?"}, 160 | "IXX": {"name": "ISE National-100", "country": "?"}, 161 | "SMSI": {"name": "Madrid General", "country": "Spain"}, 162 | "OMXSPI": {"name": "Stockholm General", "country": "Sweden"}, 163 | "SSMI": {"name": "Swiss Market", "country": "Swizerland"}, 164 | "FTSE": {"name": "FTSE 100", "country": "UK"}, 165 | "CCSI": {"name": "CMA", "country": "?"}, 166 | "TA100": {"name": "TA-100", "country": "?"}, 167 | } 168 | 169 | 170 | class SymbolNotFound(Exception): 171 | pass 172 | 173 | 174 | class Cache: 175 | """ 176 | Class that provides the cache using sqllite. 177 | """ 178 | HISTORY_COL_DEF=( 179 | "stock_id integer primary key autoincrement", 180 | "symbol text not null", 181 | "date date", 182 | "open float", 183 | "close float", 184 | "low float", 185 | "high float", 186 | "volume float", 187 | "adjclose float") 188 | 189 | def __init__(self): 190 | dbPath = os.path.expanduser(CACHE) 191 | dbDir = os.path.split(dbPath)[0] 192 | if not os.path.isdir(dbDir): 193 | os.mkdir(dbDir) 194 | 195 | self.db = sqlite3.connect(dbPath) 196 | 197 | cursor = self.db.cursor() 198 | cursor.execute("CREATE TABLE IF NOT EXISTS history (%s)" % (", ".join(Cache.HISTORY_COL_DEF))) 199 | cursor.execute("CREATE UNIQUE INDEX IF NOT EXISTS ticker ON history (symbol, date ASC)") 200 | cursor.close() 201 | 202 | def symbols(self): 203 | """Return a list of symbols that have been cached.""" 204 | cursor = self.db.cursor() 205 | symbols = cursor.execute("select distinct symbol from history").fetchall() 206 | cursor.close() 207 | return [x[0] for x in symbols] 208 | 209 | def get(self, symbol, wantDate, priorDate=None): 210 | cursor = self.db.cursor() 211 | if priorDate == None: 212 | hist = cursor.execute("SELECT * FROM HISTORY WHERE (date='%s' and symbol='%s')" % (wantDate, symbol)).fetchall() 213 | else: 214 | hist = cursor.execute("SELECT * FROM HISTORY WHERE (date>='%s' and date<='%s' and symbol='%s')" % (priorDate, wantDate, symbol)).fetchall() 215 | return [Quote.fromRow(x) for x in hist] 216 | 217 | def init(self, symbol, wantDate, priorDate): 218 | d = priorDate 219 | while d <= wantDate: 220 | cursor = self.db.cursor() 221 | cursor.execute("INSERT OR REPLACE INTO HISTORY VALUES (NULL, '%s', '%s', NULL, NULL, NULL, NULL, NULL, NULL)" % (symbol, d)) 222 | cursor.close() 223 | d += 1 224 | self.db.commit() 225 | 226 | def put(self, quotes): 227 | for quote in quotes: 228 | cursor = self.db.cursor() 229 | cursor.execute("INSERT OR REPLACE INTO HISTORY VALUES (NULL, '%(symbol)s', '%(date)s', '%(open)s', %(close)f, %(low)f, %(high)f, %(volume)f, %(adjclose)f)" % quote.__dict__) 230 | cursor.close() 231 | self.db.commit() 232 | 233 | def purge(self, symbol): 234 | cursor = self.db.cursor() 235 | hist = cursor.execute("DELETE FROM HISTORY WHERE (symbol='%s')" % (symbol)).fetchall() 236 | cursor.close() 237 | self.db.commit() 238 | 239 | def __del__(self): 240 | try: 241 | self.db.commit() 242 | except: 243 | pass 244 | 245 | 246 | class Market: 247 | """ 248 | Main top-level class for YahooQuote. 249 | 250 | Holds/fetches info on a per-ticker basis 251 | 252 | Use this like a dict, where the keys are ticker symbols, 253 | and the values are Ticker objects (see class Ticker) 254 | """ 255 | def __init__(self): 256 | """ 257 | Creates a 'Market' object, which accesses quotes for 258 | various ticker symbols through Ticker object 259 | 260 | No args or keywords needed. 261 | """ 262 | self.cache = Cache() 263 | 264 | self.indexes = {} 265 | self.tickersBySymbol = {} 266 | self.symbolIndex = {} 267 | 268 | def loadIndexes(self): 269 | """ 270 | loads all index components. 271 | 272 | The first time this method is executed, it will download 273 | a list of component stocks for each index and store them in the 274 | database. 275 | 276 | Subsequent calls to this method, even in future sessions, will 277 | just load these component stock lists from the database 278 | """ 279 | logging.debug("loading indexes and components") 280 | # load up all the indexes and their parts 281 | for symbol in indexes.keys(): 282 | idx = Index(self, symbol, **indexes[symbol]) 283 | self.indexes[symbol] = idx 284 | for stocksym in idx.components: 285 | self.symbolIndex[stocksym] = symbol 286 | logging.debug(" index components loaded") 287 | 288 | 289 | def __getitem__(self, symbol): 290 | """ 291 | If 'symbol' is an index, returns an L{Index} object for that index symbol. 292 | 293 | If 'symbol' is an individual stock symbol, then returns a L{Ticker} 294 | object for that stock 295 | """ 296 | su = symbol.upper() 297 | if su in self.indexes: 298 | return self.indexes[su] 299 | 300 | # we store weak refs to the tickers, to save memory when 301 | # the tickers go away 302 | ticker = None 303 | ref = self.tickersBySymbol.get(symbol, None) 304 | if ref: 305 | ticker = ref() 306 | 307 | if not ticker: 308 | ticker = Ticker(self, symbol) 309 | self.tickersBySymbol[symbol] = weakref.ref(ticker) 310 | 311 | return ticker 312 | 313 | 314 | def fetchHistory(self): 315 | """ 316 | Fetches all history for all known stocks 317 | 318 | This can take an hour or more, even with a broadband connection, and 319 | will leave you with a cache file of over 100MB. 320 | 321 | If you're only interested in specific stocks or indexes, you might prefer 322 | to invoke L{Index.fetchHistory} or L{Ticker.FetchHistory}, respectively. 323 | """ 324 | logging.info("fetching history for all stocks") 325 | if len(self.indexes.values()) == 0: 326 | self.loadIndexes() 327 | 328 | try: 329 | for index in self.indexes.values(): 330 | # fill the queue with callable methods 331 | logging.debug("fetching index %s", index) 332 | index.fetchHistory() 333 | except KeyboardInterrupt: 334 | logging.info("interrupted by user") 335 | 336 | def updateHistory(self): 337 | """ 338 | Updates all known stocks' histories. Don't run this unless 339 | you have previously invoked L{Market.fetchHistory} 340 | """ 341 | for symbol in self.cache.symbols(): 342 | logging.debug("updating symbol %s", symbol) 343 | ticker = self[symbol] 344 | ticker.updateHistory() 345 | 346 | 347 | class Index: 348 | """ 349 | encapsulates a stock index, eg 'dji' for dow jones 350 | """ 351 | def __init__(self, market, symbol, **kw): 352 | """ 353 | Creates a market index container. You shouldn't normally have to 354 | create these yourself. 355 | """ 356 | symbol = symbol.lower() 357 | self.market = market 358 | self.symbol = symbol 359 | self.__dict__.update(kw) 360 | 361 | # fetch the components from yahoo 362 | self.components = [] 363 | self.fetchFromYahoo() 364 | 365 | def __repr__(self): 366 | 367 | return "" % self.symbol 368 | 369 | 370 | def load(self, row): 371 | """ 372 | loads component names from database 373 | """ 374 | for symbolRow in row.stocks: 375 | self.components.append(symbolRow.symbol) 376 | 377 | def fetchFromYahoo(self): 378 | """ 379 | retrieves a list of component stocks for this index from the 380 | Yahoo Finance website 381 | """ 382 | logging.debug("Refreshing list of component stocks for %s" % self.symbol) 383 | 384 | # create args 385 | parmsDict = { 386 | "s": "@^"+self.symbol.upper(), 387 | "f": "sl1d1t1c1ohgv", 388 | "e": ".csv", 389 | "h": 0, 390 | } 391 | parms = "&".join([("%s=%s" % (k,v)) for k,v in parmsDict.items()]) 392 | 393 | # construct full URL 394 | url = "http://download.finance.yahoo.com/d/quotes.csv?%s" % parms 395 | 396 | # get the CSV for this index 397 | f = urllib.urlopen(url) 398 | lines = f.readlines() 399 | csvReader = csv.reader(lines) 400 | 401 | # extract the component symbols 402 | componentSymbols = [item[0].lower() for item in csvReader if item] 403 | 404 | # save to database 405 | for symbol in componentSymbols: 406 | self.components.append(symbol) 407 | 408 | def fetchHistory(self): 409 | """ 410 | Invokes L{Ticker.fetchHistory} on all component stocks of this index 411 | """ 412 | logging.debug("index %s" % self.symbol) 413 | 414 | for sym in self.components: 415 | 416 | ticker = self.market[sym] 417 | ticker.fetchHistory() 418 | 419 | def __getitem__(self, symbol): 420 | """ 421 | retrieves a component ticker 422 | """ 423 | return self.market[self.components[symbol]] 424 | 425 | 426 | 427 | class Ticker: 428 | """ 429 | Represents the prices of a single ticker symbol. 430 | 431 | Works as a smart sequence, keyed by yyyymmdd numbers 432 | 433 | The magic attribute 'now' fetches current prices 434 | """ 435 | baseUrlQuote = baseUrlQuote 436 | baseUrlHistory = baseUrlHistory 437 | 438 | def __init__(self, market, symbol, index=None): 439 | """ 440 | Create a Ticker class, which gets/caches/retrieves quotes 441 | for a single ticker symbol 442 | 443 | Treat objects of this class like an array, where you can get 444 | a single item (date) to return a Quote object for the symbol 445 | and date, or get a slice, to return a list of Quote objects 446 | for the date range. 447 | 448 | Note - you shouldn't instantiate this class directly, but instead 449 | get an instance by indexing a L{Market} object - see 450 | L{Market.__getitem__} 451 | """ 452 | self.market = market 453 | 454 | self.symbol = symbol.lower() 455 | index = index or self.market.symbolIndex.get(symbol, None) 456 | if isinstance(index, str): 457 | index = self.market[index] 458 | self.index = index 459 | 460 | self.dates = {} 461 | 462 | self.firstKey = "first" 463 | self.lastKey = "last" 464 | 465 | def getQuote(self): 466 | """ 467 | Returns a Quote object for this stock 468 | """ 469 | # construct the query URL 470 | #?s=MSFT&f=sl1d1t1c1ohgv&e=.csv 471 | baseUrl = self.baseUrlQuote 472 | parms = [ 473 | "s=%s" % self.symbol, 474 | "f=sl1d1t1c1ohgv", 475 | "e=.csv", 476 | ] 477 | url = baseUrl + "?" + "&".join(parms) 478 | 479 | # get the raw csv lines from Yahoo 480 | lines = urllib.urlopen(url).readlines() 481 | 482 | # and get the quote from it 483 | return Quote.fromYahooQuote(self, lines[0]) 484 | 485 | def __getitem__(self, date): 486 | """ 487 | Retrieves/creates a Quote object for this ticker's prices 488 | for a particular date 489 | """ 490 | logging.debug("date=%s", date) 491 | 492 | logging.debug("%s(1): refs=%s" % (self.symbol, self.refs)) 493 | 494 | now = QuoteDate.now() 495 | 496 | if isinstance(date, slice): 497 | # give back a slice 498 | priorDate = QuoteDate(date.start) 499 | wantDate = QuoteDate(date.stop) 500 | else: 501 | wantDate = date 502 | priorDate = None 503 | 504 | if not isinstance(wantDate, QuoteDate): 505 | wantDate = QuoteDate(wantDate) 506 | if priorDate != None and not isinstance(priorDate, QuoteDate): 507 | priorDate = QuoteDate(priorDate) 508 | 509 | # attempted prescience? 510 | if wantDate > now or priorDate > now: 511 | raise IndexError("Prescience disabled by order of Homeland Security") 512 | 513 | # no, seek it from db or yahoo 514 | quotes = self.market.cache.get(self.symbol, wantDate, priorDate) 515 | if priorDate == None: 516 | expectedQuotes = 1 517 | else: 518 | expectedQuotes = (wantDate - priorDate) + 1 519 | if len(quotes) != expectedQuotes: 520 | self._fetch(wantDate, priorDate) 521 | quotes = self.market.cache.get(self.symbol, wantDate, priorDate) 522 | 523 | if isinstance(date, slice): 524 | return quotes 525 | else: 526 | return quotes[0] 527 | 528 | def __repr__(self): 529 | if self.index: 530 | idx = "%s/" % self.index.symbol 531 | else: 532 | idx = "" 533 | return "" % (idx, self.symbol) 534 | 535 | def __getattr__(self, attr): 536 | """ 537 | Intercept attribute 'now' to mean a fetch of present prices 538 | """ 539 | if attr in ['now', 'today']: 540 | return self.getQuote() 541 | 542 | if attr == 'refs': 543 | return len(gc.get_referrers(self)) 544 | 545 | raise AttributeError(attr) 546 | 547 | def _fetch(self, wantDate, priorDate=None, checkDuplicates=True): 548 | """ 549 | fetches a range of quotes from site, hopefully 550 | including given date, and stores these in the database 551 | 552 | argument 'date' MUST be a QuoteDate object 553 | """ 554 | if not isinstance(wantDate, QuoteDate): 555 | raise Exception("Invalid date %s: not a QuoteDate" % wantDate) 556 | 557 | # go some days before and after 558 | if priorDate == None: 559 | priorDate = wantDate - fetchWindow + 1 560 | year1, month1, day1 = priorDate.toYmd() 561 | year2, month2, day2 = (wantDate + 1).toYmd() 562 | 563 | self.market.cache.init(self.symbol, wantDate, priorDate) 564 | logging.info("fetching %s for %s-%s-%s to %s-%s-%s" % ( 565 | self.symbol, 566 | year1, month1, day1, 567 | year2, month2, day2)) 568 | 569 | baseUrl = self.baseUrlHistory 570 | parms = [ 571 | "s=%s" % self.symbol.upper(), 572 | "a=%d" % (month1-1), 573 | "b=%d" % day1, 574 | "c=%d" % year1, 575 | "d=%d" % (month2-1), 576 | "e=%d" % day2, 577 | "f=%d" % year2, 578 | "g=d", 579 | "ignore=.csv", 580 | ] 581 | 582 | url = baseUrl + "?" + "&".join(parms) 583 | logging.debug(" fetching URL %s", url) 584 | #print "url=%s" % url 585 | 586 | # get the raw csv lines from Yahoo 587 | resp = urllib.urlopen(url) 588 | if resp.getcode() == 404: 589 | logging.info("%s: No history for %04d-%02d-%02d to %04d-%02d-%02d" % ( 590 | self.symbol, 591 | year1, month1, day1, 592 | year2, month2, day2)) 593 | return 594 | lines = resp.readlines() 595 | 596 | logging.debug(" fetched %s", lines) 597 | 598 | if lines[0].startswith("Date"): 599 | lines = lines[1:] 600 | 601 | quotes = [] 602 | try: 603 | quotes = [Quote.fromYahooHistory(self.symbol, line) for line in lines] 604 | except: 605 | logging.exception("Failed to process yahoo data") 606 | 607 | if len(quotes) == 0: 608 | logging.info("%s: No history for %04d-%02d-%02d to %04d-%02d-%02d" % ( 609 | self.symbol, 610 | year1, month1, day1, 611 | year2, month2, day2)) 612 | else: 613 | # sort quotes into ascending order and fill in any missing dates 614 | quotes.sort(lambda q1, q2: cmp(q1.date, q2.date)) 615 | self.market.cache.put(quotes) 616 | 617 | def fetchHistory(self, start=None, end=None): 618 | """ 619 | fetches this stock's entire history - you should only ever 620 | do this once, and thereafter, invoke 621 | L{Ticker.updateHistory} to keep the history up to date 622 | """ 623 | if start == None: 624 | startDay = QuoteDate.fromYmd(1950, 1, 1) 625 | else: 626 | startDay = QuoteDate(end) 627 | 628 | if end == None: 629 | endDay = QuoteDate.now() 630 | else: 631 | endDay = QuoteDate(start) 632 | 633 | cursor = self.market.cache.purge(self.symbol) 634 | 635 | try: 636 | # now get the whole history, lock stock and barrel 637 | self._fetch(endDay, startDay, False) 638 | except KeyboardInterrupt: 639 | raise 640 | except: 641 | logging.exception("%s: failed to fetch" % self.symbol) 642 | 643 | def updateHistory(self, start=None, end=None): 644 | """ 645 | Updates this stock's history. You should not invoke this 646 | method unless you have invoked L{Ticker.fetchHistory} at 647 | some time in the past. 648 | """ 649 | if end == None: 650 | end = QuoteDate.now() 651 | if start == None: 652 | cursor = self.market.db 653 | lastdate = cursor.execute("select MAX(date) from history where (symbol='%s')" % self.symbol).fetchone()[0] 654 | start = QuoteDate(lastdate) + 1 655 | 656 | if not isinstance(start, QuoteDate): 657 | start = QuoteDate(start) 658 | if not isinstance(end, QuoteDate): 659 | end = QuoteDate(end) 660 | 661 | if end <= start: 662 | return 663 | 664 | quotes = self.market.cache.get(self.symbol, end, start) 665 | if (len(quotes) - 1) != (end - start): 666 | self._fetch(end, start) 667 | 668 | class Quote: 669 | """ 670 | dumb object which wraps quote data 671 | """ 672 | def __init__(self, symbol, **kw): 673 | self.symbol = symbol 674 | self.date=None 675 | self.open=None 676 | self.close=None 677 | self.low=None 678 | self.high=None 679 | self.volume=None 680 | self.adjclose=None 681 | self.__dict__.update(kw) 682 | 683 | # Normalization concept from http://luminouslogic.com/how-to-normalize-historical-data-for-splits-dividends-etc.htm 684 | def get_adjopen(self): 685 | if self.adjclose: 686 | return (self.adjclose / self.close) * self.open 687 | else: 688 | return None 689 | adjopen = property(fget=get_adjopen) 690 | 691 | def get_adjlow(self): 692 | if self.adjclose: 693 | return (self.adjclose / self.close) * self.low 694 | else: 695 | return None 696 | adjlow = property(fget=get_adjlow) 697 | 698 | def get_adjhigh(self): 699 | if self.adjclose: 700 | return (self.adjclose / self.close) * self.high 701 | else: 702 | return None 703 | adjhigh = property(fget=get_adjhigh) 704 | 705 | def __repr__(self): 706 | 707 | # date = "%04d%02d%02d" % (year, month, day) 708 | # quoteDict['open'] = float(open) 709 | # quoteDict['high'] = float(high) 710 | # quoteDict['low'] = float(low) 711 | # quoteDict['close'] = float(close) 712 | # quoteDict['volume'] = float(vol) 713 | # quoteDict['adjclose'] = float(adjclose) 714 | 715 | 716 | if None in (self.open, self.close, self.low, self.high, self.adjclose, self.volume): 717 | return "" % (self.symbol, self.date) 718 | else: 719 | if self.close > self.open: 720 | m = "+%.02f" % (self.close - self.open) 721 | elif self.close < self.open: 722 | m = "%.02f" % (self.close - self.open) 723 | else: 724 | m = "0.00" 725 | return "" \ 726 | % (self.symbol, 727 | self.date, 728 | m, 729 | self.open, self.close, 730 | self.low, self.high, 731 | self.adjclose, 732 | self.volume) 733 | 734 | def fromYahooHistory(symbol, line): 735 | """ 736 | Static method - reads a raw line from yahoo history 737 | and returns a Quote object 738 | """ 739 | #logging.info("fromYahooHistory: line=%s" % repr(line)) 740 | 741 | items = csv.reader([line]).next() 742 | 743 | logging.debug("items=%s" % str(items)) 744 | 745 | ydate, open, high, low, close, vol, adjclose = items 746 | 747 | # determine date of this next result 748 | quote = Quote(symbol, 749 | date=QuoteDate.fromYahoo(ydate), 750 | open=float(open), 751 | close=float(close), 752 | low=float(low), 753 | high=float(high), 754 | volume=int(vol), 755 | adjclose=float(adjclose)) 756 | return quote 757 | 758 | fromYahooHistory = staticmethod(fromYahooHistory) 759 | 760 | def fromYahooQuote(symbol, line): 761 | """ 762 | Static method - given a ticker object and a raw quote line from Yahoo for 763 | that ticker, build and return a Quote object with that data 764 | """ 765 | # examples: 766 | # sym last d/m/y time change open high low volume 767 | # MSFT, 30.49, 6/15/2007,4:00:00 PM, -0.03, 30.88, 30.88, 30.43, 100941384 768 | # TEL.NZ,4.620, 6/15/2007,12:58am, 0.000, 4.640, 4.640, 4.590, 3692073 769 | 770 | items = csv.reader([line]).next() 771 | sym, last, date, time, change, open, high, low, volume = items 772 | 773 | # massage/convert the fields 774 | sym = sym.lower() 775 | last = float(last) 776 | 777 | day, month, year = [int(f) for f in date.split("/")] 778 | 779 | date = QuoteDate.fromYmd(year, month, day) 780 | if change.startswith("-"): 781 | change = -float(change[1:]) 782 | elif change.startswith("+"): 783 | change = float(change[1:]) 784 | else: 785 | change = float(change) 786 | open = float(open) 787 | close = last 788 | high = float(high) 789 | low = float(low) 790 | volume = float(volume) 791 | adjclose = last 792 | 793 | # got all the bits, now can wrap a Quote 794 | return Quote(sym, 795 | date=date, 796 | open=open, 797 | close=last, 798 | high=high, 799 | low=low, 800 | volume=volume, 801 | adjclose=adjclose) 802 | 803 | fromYahooQuote = staticmethod(fromYahooQuote) 804 | 805 | def fromRow(row): 806 | """ 807 | Static method - Constructs a L{Quote} object from a given sqlite row 808 | """ 809 | return Quote(symbol=row[1], 810 | date=row[2], 811 | open=row[3], 812 | close=row[4], 813 | low=row[5], 814 | high=row[6], 815 | volume=row[7], 816 | adjclose=row[8]) 817 | 818 | fromRow = staticmethod(fromRow) 819 | 820 | 821 | class QuoteDate(int): 822 | """ 823 | Simple int subclass that represents yyyymmdd quote dates 824 | """ 825 | def __new__(cls, val): 826 | """ 827 | Create a QuoteDate object. Argument can be an int or string, 828 | as long as it is in the form YYYYMMDD 829 | """ 830 | if isinstance(val, datetime.datetime): 831 | val = (val.year * 10000) + (val.month * 100) + val.day 832 | inst = super(QuoteDate, cls).__new__(cls, val) 833 | inst.year, inst.month, inst.day = inst.toYmd() 834 | return inst 835 | 836 | def __add__(self, n): 837 | """ 838 | Adds n days to this QuoteDate, and returns a new QuoteDate object 839 | """ 840 | return QuoteDate.fromUnix(self.toUnix() + n * DAYSECS) 841 | 842 | def __sub__(self, n): 843 | """ 844 | Subtracts n days from this QuoteDate, and returns a new QuoteDate object 845 | """ 846 | if isinstance(n, QuoteDate): 847 | return int((self.toUnix() - n.toUnix()) / DAYSECS) 848 | else: 849 | return QuoteDate.fromUnix(self.toUnix() - n * DAYSECS) 850 | 851 | def toUnix(self): 852 | """ 853 | Converts this QuoteDate object to a unix 'seconds since epoch' 854 | time 855 | """ 856 | return time.mktime(self.toYmd() + (0,0,0,0,0,0)) 857 | 858 | def fromUnix(udate): 859 | """ 860 | Static method - converts a unix 'seconds since epoch' 861 | date into a QuoteDate string 862 | """ 863 | return QuoteDate(time.strftime("%Y%m%d", time.localtime(float(udate)))) 864 | 865 | fromUnix = staticmethod(fromUnix) 866 | 867 | def toDateTime(self): 868 | return datetime.date.fromtimestamp(self.toUnix()) 869 | 870 | def now(): 871 | """ 872 | Static method - returns a QuoteDate object for today 873 | """ 874 | return QuoteDate.fromUnix(time.time()) 875 | 876 | now = staticmethod(now) 877 | 878 | def toYmd(self): 879 | """ 880 | returns tuple (year, month, day) 881 | """ 882 | s = "%08d" % self 883 | return int(s[0:4]), int(s[4:6]), int(s[6:8]) 884 | 885 | def fromYmd(year, month, day): 886 | """ 887 | Static method - instantiates a QuoteDate 888 | set to given year, month, day 889 | """ 890 | return QuoteDate("%04d%02d%02d" % (int(year), int(month), int(day))) 891 | 892 | fromYmd = staticmethod(fromYmd) 893 | 894 | def fromYahoo(ydate): 895 | """ 896 | Static method - converts a 'yyyy-mmm-dd' 897 | yahoo date into a QuoteDate object 898 | """ 899 | dateFields = ydate.split("-") 900 | year = int(dateFields[0]) 901 | try: 902 | monthStr = MONTH2NUM[dateFields[1]] 903 | except: 904 | monthStr = dateFields[1] 905 | month = int(monthStr) 906 | day = int(dateFields[2]) 907 | return QuoteDate.fromYmd(year, month, day) 908 | 909 | fromYahoo = staticmethod(fromYahoo) 910 | --------------------------------------------------------------------------------