Finance-Quote-Yahoo 0.1 → 0.2
raw patch · 2 files changed
+218/−54 lines, 2 filesdep +time
Dependencies added: time
Files
- Finance-Quote-Yahoo.cabal +2/−2
- Finance/Quote/Yahoo.hs +216/−52
Finance-Quote-Yahoo.cabal view
@@ -1,5 +1,5 @@ Name: Finance-Quote-Yahoo-Version: 0.1+Version: 0.2 Description: Obtain quote data from finance.yahoo.com Synopsis: Obtain quote data from finance.yahoo.com Category: Web@@ -9,6 +9,6 @@ License-file: LICENSE Author: brad clawsie Maintainer: haskell@fastmail.fm-Build-Depends: base, network, HTTP, HTTP-Simple, MissingH+Build-Depends: base, network, HTTP, HTTP-Simple, MissingH, time>=1.1.1 Exposed-modules: Finance.Quote.Yahoo ghc-options: -O
Finance/Quote/Yahoo.hs view
@@ -4,40 +4,79 @@ finance.yahoo.com, which delivers a csv file with data for various fields, which are documented at http:\/\/www.gummy-stuff.org\/Yahoo-data.htm. -The only exported function is getQuote, which takes a list of quote symbols -(in the finance sense of \"symbol\" - YHOO,GOOG etc), a list of fields, and -returns a list of pairs for each symbol. Upon any problem, Nothing is -returned.+The homepage for this module is +http:\/\/www.b7j0c.org\/content\/haskell-yquote.html +The license for this module is at+http:\/\/www.b7j0c.org\/content\/license.txt++Since this uses Data.Time.Format, ghc-6.6.1 or greater is required.++Error reporting is somewhat of a mixed model in this module. Where strict+errors of data construction occur, these will be noted as fatal error()+signals, so the error can be noted and fixed. An example of this would be+putting the start and end data in the wrong order for the retrieval of+historical quotes or the creation of a malformed URI. On the other hand,+I continue to propogate Nothing() for networking issues as there may be +external issues creating these errors for which one may want program +execution to continue. My personal tendency is to fail early when +possible and practical.+++Exported functions:++getQuote, which takes a list of quote symbols (in the finance sense of +\"symbol\" - YHOO,GOOG etc), a list of fields, and +returns a Data.Map, where the keys are pairs (symbol,field) and +values are the returned Strings. Upon any problem, Nothing is +returned. I have not cast the data into stronger types than String since+Yahoo is inconsistent about what is returned in the csv. Fields often+contain punctuation, symbols, as well as numbers. So really, they are+Strings.++getHistoricalQuote, which takes a quote symbol, and two Data.Time.Calendar+Day types, one for the starting date to receive quote data, and one for the+end date. Yahoo does not let you choose the fields to see in historical+quotes, data is limited to price and volume information.+ Here is small complete program illustrating the use of this module @ module Main where import Finance\.Quote\.Yahoo- quoteSymbolList = \[\"YHOO\",\"^DJI\"\] :: [QuoteSymbol]- quoteFieldsList = \[\"s\",\"l1\",\"c\"\] :: [QuoteField]+ import Data\.Time\.Calendar+ import Data\.Map+ quoteSymbolList = [\"YHOO\",\"^DJI\"] :: [QuoteSymbol]+ quoteFieldsList = [\"s\",\"l1\",\"c\"] :: [QuoteField] main = do- q <- getQuote quoteSymbolList quoteFieldsList- case q of- Nothing -> error \"no return\"- Just l -> print l- return ()-@--which outputs:--@- \[\[(\"s\",\"YHOO\"),(\"l1\",\"26.69\"),(\"c\",\"-0.28 - -1.04%\")\],- \[(\"s\",\"^DJI\"),(\"l1\",\"13577.87\"),(\"c\",\"+76.17 - +0.56%\")\]\]+ q <- getQuote quoteSymbolList quoteFieldsList+ case q of+ Nothing -> error \"no map\"+ Just m -> case (Data.Map.lookup (\"YHOO\",\"l1\") m) of+ Nothing -> print \"no match\"+ Just a -> print a+ let startDate = Data.Time.Calendar.fromGregorian 2007 07 01+ let endDate = Data.Time.Calendar.fromGregorian 2007 07 03+ h <- getHistoricalQuote (head quoteSymbolList) startDate endDate+ case h of+ Nothing -> error \"no historical\"+ Just l -> sequence $ Prelude.map print l+ return () @ -}-module Finance.Quote.Yahoo (getQuote,defaultFields,- QuoteField,QuoteSymbol,QuoteValue) where+module Finance.Quote.Yahoo (getQuote,getHistoricalQuote,defaultQuoteFields,+ baseQuoteURI,baseHistoricalURI,+ QuoteField,QuoteSymbol,QuoteValue,Quote,+ QuoteCurrency,HistoricalQuote) where import qualified Network.HTTP.Simple as H (httpGet) -import qualified Data.String as S (join,replace,split)+import qualified Data.String as S (join,split) import qualified Network.URI as U (parseURI,escapeURIString, isUnescapedInURI) +import qualified Data.Time.Calendar as T (Day(..),fromGregorian)+import qualified Data.Time.Format as F (formatTime)+import qualified System.Locale as L (defaultTimeLocale)+import qualified Data.Map as M (fromList,Map(..)) {- License info:@@ -47,52 +86,177 @@ -} --- | This is the base uri to get csv quotes. -baseURI = "http://finance.yahoo.com/d/quotes.csv" :: String- type QuoteField = String type QuoteSymbol = String type QuoteValue = String+type QuoteCSV = String+type Quote = [(QuoteField,QuoteValue)] --- | If you just want the symbol, closing price and change, use this.-defaultFields = ["s","l1","c"] :: [QuoteField] +-- | fetchCSV is a convenience function broken out to isolate HTTP use.+fetchCSV :: QuoteSymbol -> IO (Maybe String)+fetchCSV s = case U.parseURI s of+ Nothing -> error("uri malformed:" ++ s)+ Just uri -> H.httpGet uri +-- | This is the base uri to get csv quotes. Exported. +baseQuoteURI = "http://finance.yahoo.com/d/quotes.csv" :: String++-- | If you just want the name, latest price and change, use this. Exported.+defaultQuoteFields = ["n","l1","c"] :: [QuoteField] + -- | quoteReq will build a String representation of a Yahoo Finance CSV -- request URI. quoteReq :: [QuoteSymbol] -> [QuoteField] -> String quoteReq symbols fields = U.escapeURIString U.isUnescapedInURI - (baseURI ++ "?s=" ++ - (S.join "+" symbols) ++ "&f=" ++ (concat fields))+ $ baseQuoteURI ++ "?s=" ++ + (S.join "+" symbols) ++ "&f=" ++ (concat fields) --- | getQuote takes two args - the symbols, and list of the fields you want. --- The return value is a list of lookup lists, one list per symbol requested,--- that match the fields you specified with their values.--- For example, if you were to provide +-- | parseQuote will take a list of symbols, a list of fields, and the +-- csv data and return a Data.Map, as described below (see getQuote).+-- If there is a mismatch in the number of fields being zipped to+-- produce the map, an error is triggered.+parseQuote :: [QuoteSymbol] -> [QuoteField] -> QuoteCSV ->+ Maybe (M.Map (QuoteSymbol, QuoteField) QuoteValue)+parseQuote symbols fields csv = + let l = concat $ map (S.split ",") $ lines $ + filter (\c -> notElem c "\r\"") csv+ p = [(x,y) | x <- symbols, y <- fields] in+ case (length p == length l) of+ True -> Just (M.fromList $ zip p l)+ False -> error("mismatch in fields and returned data")++-- | getQuote will take a list of symbols, a list of fields, and will +-- return a Data.Map, where the key type is+-- (symbol,field) +-- and the value type is whatever quote value string is returned.+-- An example map entry: ----- \[\"^DJI\",\"YHOO\"\] as the symbols+-- key: \(\"YHOO\",\"c\"\), value: \"24.00\" ----- \[\"s\",\"c\"\] as the fields, +-- Which gives you the closing price (c) for the symbol YHOO. ----- the response structure would be+-- NOTE!+-- This function does NOT alter the casing of the quote symbols passed+-- in the first parameter. These symbols are used as the first element+-- of the Map key tuple without altering them. Be careful! This function+-- is exported.+getQuote :: [QuoteSymbol] -> [QuoteField] ->+ IO (Maybe (M.Map (QuoteSymbol, QuoteField) QuoteValue))+getQuote symbols fields =+ do+ trycsv <- fetchCSV (quoteReq symbols fields)+ case trycsv of+ Nothing -> return Nothing+ Just csv -> return $ parseQuote symbols fields csv++-- | This is the base uri to get csv historical quote data. Exported.+baseHistoricalURI = "http://ichart.finance.yahoo.com/table.csv"++-- | Float is not an fully appropriate currency type, beware. Exported.+type QuoteCurrency = Float++-- | HistoricalQuote reflects the row form of a yahoo historical quote:+-- Date,Open,High,Low,Close,Volume,Adj Close (taken from the csv itself).+-- Exported.+data HistoricalQuote = + HistoricalQuote { + symbol :: QuoteSymbol,+ date :: T.Day,+ open :: QuoteCurrency,+ high :: QuoteCurrency,+ low :: QuoteCurrency,+ close :: QuoteCurrency,+ adjclose :: QuoteCurrency,+ volume :: Int+ } deriving Show++-- | historicalQuoteReq will build a String representation of a +-- Yahoo Finance CSV historical quote request URI. +historicalQuoteReq :: QuoteSymbol -> T.Day -> T.Day -> String+historicalQuoteReq symbol start end = + let (startDay,startMonth,startYear) = dateArgs start+ (endDay,endMonth,endYear) = dateArgs end in+ U.escapeURIString U.isUnescapedInURI + $ baseHistoricalURI ++ "?s=" ++ symbol +++ "&a=" ++ startMonth ++ "&b=" ++ startDay ++ "&c=" ++ startYear +++ "&d=" ++ endMonth ++ "&e=" ++ endDay ++ "&f=" ++ endYear+ where+ -- Return the string args for the URI.+ -- Note when parsing months - for some odd reason Yahoo has decided + -- that month numbers should be zero-based...06 = July, etc.+ dateArgs :: T.Day -> (String,String,String)+ dateArgs t = (day,month,year) where + dtl = L.defaultTimeLocale + day = F.formatTime dtl "%d" t+ month = show $ (read (F.formatTime dtl "%m" t) :: Int) - 1+ year = F.formatTime dtl "%Y" t++-- | parseHistorical takes the raw csv from Yahoo Finance and returns +-- a list of HistoricalQuote entries.+parseHistorical :: QuoteSymbol -> QuoteCSV -> Maybe [HistoricalQuote]+parseHistorical symbol' csv = + let l = reverse $ map (S.split ",") $+ (tail . lines) $ filter (\c -> notElem c "\r") csv in+ Just $ map (makeHistoricalQuote) l+ where+ -- Create a HistoricalQuote entry from a line from the csv.+ makeHistoricalQuote :: [String] -> HistoricalQuote+ makeHistoricalQuote l = + case (length l == 7) of+ False -> error("malformed line:" ++ (show l))+ True -> let date' = makeDay (l!!0)+ open' = read (l!!1) :: QuoteCurrency+ high' = read (l!!2) :: QuoteCurrency+ low' = read (l!!3) :: QuoteCurrency+ close' = read (l!!4) :: QuoteCurrency+ adjclose' = read (l!!6) :: QuoteCurrency+ volume' = read (l!!5) :: Int + in HistoricalQuote { symbol = symbol',+ date = date',+ open = open',+ high = high',+ low = low',+ close = close',+ adjclose = adjclose',+ volume = volume' }+ where+ -- Create a Day type from the str date from the csv.+ makeDay :: String -> T.Day+ makeDay s = + let a = S.split "-" s in+ case (length a == 3) of + False -> error("date field " ++ s ++ " malformed")+ True -> let y = read (a!!0) :: Integer+ m = read (a!!1) :: Int+ d = read (a!!2) :: Int in+ T.fromGregorian y m d++-- | getHistoricalQuote takes a stock symbol and a start and end dates+-- and obtains the HistoricalQuote lines for this given range.+-- Nothing is returned on any error. Check finance.yahoo.com to see how+-- far they offer quote history for a symbol you are interested in.+-- Note! Yahoo takes some liberties with dates due to weekends and +-- holidays and market closures. Exported. ----- \[\[(\"s\",\"^DJI\"),(\"c\",\"13,500\")\],--- \[(\"s\",\"YHOO\"),(\"c\",\"27.00\")\]\]+-- Here is what a sample result looks like for one day in the history: ----- As this function is in the Maybe Monad, Nothing is returned on an error.-getQuote :: [QuoteSymbol] -> [QuoteField] -> - IO (Maybe [[(QuoteField,QuoteValue)]])-getQuote symbols fields = - let s = (quoteReq symbols fields) in - case U.parseURI s of- Nothing -> return Nothing- Just uri -> do - trycsv <- H.httpGet uri+-- HistoricalQuote \{symbol \= \"YHOO\",+-- date \= 2007-07-02\, +-- open \= 27.19\, +-- high \= 27.27\, +-- low \= 26.76\, +-- close \= 26.86\, +-- adjclose \= 26.86\, +-- volume \= 21011000\}+--+getHistoricalQuote :: QuoteSymbol -> T.Day -> T.Day -> + IO (Maybe [HistoricalQuote])+getHistoricalQuote symbol start end = + case end > start of+ False -> error("start date must be earlier than end date")+ True -> do + trycsv <- fetchCSV (historicalQuoteReq symbol start end) case trycsv of Nothing -> return Nothing- Just csv ->- return (Just (map (zip fields)- (map (S.split ",") - (lines (S.replace "\r" "" - (S.replace "\"" "" csv)))))) - + Just csv -> return $ parseHistorical symbol csv