#!/usr/bin/python
#
# Author: TFB (http://thefinancebuff.com)
#
# This script retrieves price quotes for a list of stock and mutual fund ticker symbols from Yahoo! Finance.
# It creates a dummy OFX file and then imports the file to the default application associated with the .ofx extension.
# I wrote this script in order to use Microsoft Money after the quote downloading feature is disabled by Microsoft.
#
# For more information, see
#    http://thefinancebuff.com/2009/09/security-quote-script-for-microsoft-money.html

import os, time, urllib2, uuid, shlex, re, datetime
import sys

currency = "USD"

stocks = ["AMZN", 
          "GOOG", 
          "PG", 
          "YHOO"]

funds = ["VTSMX", 
         "VBMFX"]

join = str.join

def _field(tag,value):
    return "<"+tag+">"+value

def _tag(tag,*contents):
    return join("\r\n",["<"+tag+">"]+list(contents)+["</"+tag+">"])

def _date():
    return time.strftime("%Y%m%d%H%M%S",time.localtime())

def _genuuid():
    return uuid.uuid4().hex

class Security:
    """
    Encapsulate a stock or mutual fund. A Security has a ticker, a name, a price quote, and 
    the as-of date and time for the price quote. Name, price and as-of date and time are retrieved
    from Yahoo! Finance.
    """

    def __init__(self, ticker):
        self.ticker = ticker
    
    def _removeIllegalChars(self, inputString):
        pattern = re.compile("[^a-zA-Z0-9 ,.-]+")
        return pattern.sub("", inputString)
        
    def getQuote(self):
        """
        Get name, price quote, and the as-of date and time for the price quote from Yahoo! Finance.
        """
        
        print self.ticker + "............."
        url = "http://finance.yahoo.com/d/quotes.csv?s=%s&f=nl1d1t1" % self.ticker
        csv = urllib2.urlopen(url).read()
        
        # example: "Amazon.com, Inc.",78.46,"9/3/2009","4:00pm"
        # can't simply use split(",") because the security name has an embedded comma
        lexer = shlex.shlex(csv)
        lexer.whitespace = ","
        lexer.whitespace_split = True

        quote = []
        for value in lexer:
            quote.append(value.strip('"'))
        
        # ampersand character (&) is not valid in OFX
        self.name = self._removeIllegalChars(quote[0])
        if self.name == "":
        	self.name = self.ticker
        self.price = quote[1]
        timeStruct = time.strptime(quote[2] + " " + quote[3], "%m/%d/%Y %I:%M%p")
        self.quoteTime = time.strftime("%Y%m%d%H%M", timeStruct) + "00.000[-5:EST]"
        print self.price

class OfxWriter:
    """
    Create an OFX file based on a list of stocks and mutual funds.
    """
    
    def __init__(self, stockList, mfList):
        self.stockList = stockList
        self.mfList = mfList
        
    def _signOn(self):
        """Generate signon message"""
    
        return _tag("SIGNONMSGSRSV1",
                    _tag("SONRS",
                         _tag("STATUS",
                             _field("CODE", "0"),
                             _field("SEVERITY", "INFO"),
                             _field("MESSAGE","Successful Sign On")
                         ),
                         _field("DTSERVER", _date()),
                         _field("LANGUAGE", "ENG"),
                         _field("DTPROFUP", "20010918083000"),
                         _tag("FI", _field("ORG", "broker.com"))
                     )
               )

    def _invPosList(self):
        posstock = []
        for stock in self.stockList:
            posstock.append(self._pos("stock", stock.ticker, stock.price, stock.quoteTime))

        posmf = []
        for mf in self.mfList:
            posmf.append(self._pos("mf", mf.ticker, mf.price, mf.quoteTime))
            
        return _tag("INVPOSLIST",
                    join("", posstock),
                    join("", posmf)
               )

    def _pos(self, type, ticker, price, quoteTime):
        return _tag("POS" + type.upper(),
                   _tag("INVPOS",
                       _tag("SECID",
                           _field("UNIQUEID", ticker),
                           _field("UNIQUEIDTYPE", "TICKER")
                       ),
                       _field("HELDINACCT", "CASH"),
                       _field("POSTYPE", "LONG"),
                       _field("UNITS", "0"),
                       _field("UNITPRICE", price),
                       _field("MKTVAL", price),
                       _field("DTPRICEASOF", quoteTime)
                   )
               )

    def _invStmt(self, currency):
        tomorrow = datetime.datetime.today() + datetime.timedelta(days=1)
        res = _tag("INVSTMTRS",
                   _field("DTASOF", tomorrow.strftime("%Y%m%d")),
                   _field("CURDEF", currency),
                   _tag("INVACCTFROM",
                      _field("BROKERID", "dummybroker.com"),
                      _field("ACCTID","0123456789")
                   ),
                   self._invPosList()
               )

        return self._message("INVSTMT","INVSTMT",res)

    def _message(self,msgType,trnType,response):
        return _tag(msgType+"MSGSRSV1",
                    _tag(trnType+"TRNRS",
                         _field("TRNUID",_genuuid()),
                         _tag("STATUS",
                             _field("CODE", "0"),
                             _field("SEVERITY", "INFO")
                         ),
                         _field("CLTCOOKIE","4"),
                         response
                     )
                )

    def _secList(self):
        stockinfo = []
        for stock in self.stockList:
            stockinfo.append(self._info("stock", stock.ticker, stock.name, stock.price))

        mfinfo = []
        for mf in self.mfList:
            mfinfo.append(self._info("mf", mf.ticker, mf.name, mf.price))
        

        return _tag("SECLISTMSGSRSV1",
                   _tag("SECLIST",
                        join("", stockinfo),
                        join("", mfinfo)
                   )
               )

    def _info(self, type, ticker, name, price):
        secInfo = _tag("SECINFO",
                       _tag("SECID",
                           _field("UNIQUEID", ticker),
                           _field("UNIQUEIDTYPE", "TICKER")
                       ),
                       _field("SECNAME", name),
                       _field("TICKER", ticker),
                       _field("UNITPRICE", price)
                   )
        if type.upper() == "MF":
            info = _tag(type.upper() + "INFO",
                       secInfo,
                       _field("MFTYPE", "OPENEND")
                   )
        else:
            info = _tag(type.upper() + "INFO", secInfo)

        return info
        
    
    def _header(self):
        return join("\r\n",[ "OFXHEADER:100",
                           "DATA:OFXSGML",
                           "VERSION:102",
                           "SECURITY:NONE",
                           "ENCODING:USASCII",
                           "CHARSET:1252",
                           "COMPRESSION:NONE",
                           "OLDFILEUID:NONE",
                           "NEWFILEUID:"+_genuuid(),
                           ""])

    def createContent(self, currency):
        return join("\r\n",[self._header(),
                            _tag("OFX",
                                self._signOn(),
                                self._invStmt(currency),
                                self._secList()
                            )])

    def writeFile(self, name, content):
        if 1:
            f = file(name,"w")
            f.write(content)
            f.close()
        else:
            print content
        # ...

if __name__=="__main__":
    stockList = []
    for ticker in stocks:
        sec = Security(ticker)
        sec.getQuote()
        stockList.append(sec)
        
    mfList = []
    for ticker in funds:
        sec = Security(ticker)
        sec.getQuote()
        mfList.append(sec)
        
    writer = OfxWriter(stockList, mfList)
    content = writer.createContent(currency)
    fileName = "quotes.ofx"
    writer.writeFile(fileName, content)

    os.startfile(fileName)


