├── .gitignore ├── autodoc-params └── templates │ ├── local-toc.html │ ├── sub-namespace-api.html │ ├── master-toc.html │ ├── overview.html │ ├── api-index.html │ ├── namespace-api.html │ └── layout.html ├── test └── stockings │ └── test │ └── core.clj ├── src └── stockings │ ├── utils.clj │ ├── exchanges.clj │ ├── alt.clj │ └── core.clj ├── README.rst ├── project.clj └── LICENSE.html /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | autodoc/** 4 | /lib/ 5 | /classes/ 6 | .lein-deps-sum 7 | -------------------------------------------------------------------------------- /autodoc-params/templates/local-toc.html: -------------------------------------------------------------------------------- 1 |
2 |

Table of Contents

3 |
4 | Section name 5 |
6 | Index entry 7 |
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /autodoc-params/templates/sub-namespace-api.html: -------------------------------------------------------------------------------- 1 |

Sub-namespace

2 |
docstr here
3 | Added in Clojure 1.1
4 | Deprecated since Clojure 1.1
5 | See also: 6 | 7 | see-also-text 8 |
9 |

10 |
11 |
12 |

Var-name

13 | Type goes here (var, multimethod, etc.)
14 |
Arglists go here
15 |
docstr here
16 | Added in Clojure 1.1
17 | Deprecated since Clojure 1.1
18 | Source 19 |
20 | -------------------------------------------------------------------------------- /autodoc-params/templates/master-toc.html: -------------------------------------------------------------------------------- 1 |
2 | 25 | -------------------------------------------------------------------------------- /autodoc-params/templates/overview.html: -------------------------------------------------------------------------------- 1 |

API Overview - Project Version (Status)

2 |
3 |
4 |
5 |
6 |
7 |
8 |

Namespace

9 | by the author
10 | Detailed API documentation
11 |
docstr here
12 | Added in Clojure 1.1
13 | Deprecated since Clojure 1.1
14 | See also: 15 | 16 | see-also-text 17 |
18 |
19 | Public variables and functions: 20 | var-name
21 |
Variables and functions in 22 | subspace name: 23 | 24 | var-name 25 | 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /autodoc-params/templates/api-index.html: -------------------------------------------------------------------------------- 1 |

Index of Public Functions and Variables - Project Version (Status)

2 | This page has an alphabetical index of all the documented functions and variables 3 | in no project name specified. 4 | 5 | 10 | 11 |
12 | Shortcuts:
13 | A B C D 14 | E F G H 15 | I J K L 16 | M 17 |
18 | N O P Q 19 | R S T U 20 | V W X Y 21 | Z 22 |
23 | Other 24 |
25 |
26 | 27 |
28 |

A

29 |
30 |  name
31 |   
32 |
33 | 36 | -------------------------------------------------------------------------------- /autodoc-params/templates/namespace-api.html: -------------------------------------------------------------------------------- 1 |

API for my-namespace 2 | - Project Version (Status) 3 |

4 | by the author
5 |
Full namespace name: my-namespace 6 |

7 |

Overview

8 |
docstr here
9 | Added in Clojure 1.1
10 | Deprecated since Clojure 1.1
11 | See also: 12 | 13 | see-also-text 14 |
15 |

16 | Related documentation: 17 | 18 |
external-doc-text 19 |

20 |

Public Variables and Functions

21 |
22 |
23 |
24 |

Var-name

25 | Type goes here (var, multimethod, etc.)
26 |
Arglists go here
27 |
docstr here
28 | Added in Clojure 1.1
29 | Deprecated since Clojure 1.1
30 | Source 31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /autodoc-params/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Overview 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 27 | 28 | 35 | 36 |
37 | 42 |
43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 |
52 |
53 | 58 |
59 |
Logo & site design by Tom Hickey.
60 | Clojure auto-documentation system by Tom Faulhaber.
61 |
62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /test/stockings/test/core.clj: -------------------------------------------------------------------------------- 1 | (ns stockings.test.core 2 | (:use clojure.test 3 | stockings.core) 4 | (:import [org.joda.time LocalDate])) 5 | 6 | (deftest test-stock-symbol-ops 7 | (testing "Basic operations on stock symbols" 8 | (is (= (prefix-stock-symbol "abcd" "efgh") "abcd:efgh")) 9 | (is (= (prefix-stock-symbol nil "abcd") "abcd")) 10 | (are [x y] (= (explode-stock-symbol x) y) 11 | "abcd" [nil "abcd"] 12 | "abcd:efgh" ["abcd" "efgh"] 13 | ":abcd" [nil ":abcd"]) 14 | (are [x y] (= (bare-stock-symbol x) y) 15 | "abcd" "abcd" 16 | "abcd:efgh" "efgh" 17 | "ab:cd:ef:gh" "gh" 18 | ":abcd" ":abcd" 19 | "abcd:" "abcd:"))) 20 | 21 | (deftest test-current-stock-quotes 22 | (testing "Current stock quotes" 23 | (let [yhoo (get-quote "yhoo")] 24 | (is (= (:symbol yhoo "YHOO"))) 25 | (is (= (:name yhoo) "Yahoo! Inc."))) 26 | (is (not (get-quote "invalid"))) 27 | (let [stock-quotes (get-quotes "yhoo" "invalid" "goog")] 28 | (is (= (count stock-quotes) 3)) 29 | (let [[yhoo invalid goog] stock-quotes] 30 | (is (= (:symbol yhoo "YHOO"))) 31 | (is (not invalid)) 32 | (is (= (:symbol goog "GOOG"))))) 33 | (let [kmap {:symbol :symbol, :name :Name, :ask :Ask, :volume :Volume} 34 | parser (build-quote-parser kmap) 35 | intc (get-quote parser "intc")] 36 | (is (= (sort (keys kmap)) (sort (keys intc)))) 37 | (is (= (:symbol intc) "INTC")) 38 | (is (= (:name intc) "Intel Corporation"))))) 39 | 40 | (deftest test-historical-stock-quotes 41 | (testing "Historical stock quotes" 42 | (let [stock-quotes (get-historical-quotes "yhoo" 43 | (LocalDate. 2010 1 1) 44 | (LocalDate. 2011 4 30)) 45 | quotes-map (to-sorted-map :date stock-quotes) 46 | lookup-quote (partial get-largest-lte quotes-map)] 47 | (is (= (count stock-quotes) 334)) 48 | (is (= (lookup-quote (LocalDate. 2011 4 29)) 49 | (lookup-quote (LocalDate. 2011 4 30)))) 50 | (is (= (:close (lookup-quote (LocalDate. 2011 3 18))) 16.03))))) 51 | 52 | (deftest test-industries 53 | (testing "Industries and industry sectors" 54 | (let [sectors (get-industry-sectors) 55 | sector-names (set ["Basic Materials" "Conglomerates" "Consumer Goods" 56 | "Financial" "Healthcare" "Industrial Goods" 57 | "Services" "Technology" "Utilities"])] 58 | (is (= (set (map :name sectors)) sector-names))) 59 | (let [i112 (get-industry 112)] 60 | (is (= (:id i112) "112")) 61 | (is (= (:name i112) "Agricultural Chemicals")) 62 | (is (contains? (set (:companies i112)) 63 | {:name "Terra Nitrogen Company, L.P." 64 | :symbol "TNH"}))) 65 | (is (not (get-industry 12345))) 66 | (is (not (get-industry "abc"))))) 67 | 68 | (deftest test-currency-exchange-rates 69 | (testing "Currency exchange rates" 70 | (let [r (get-exchange-rate :usd :eur)] 71 | (is (< 0.5 (:rate r) 1.0))))) 72 | 73 | (deftest test-stock-symbol-suggestions 74 | (testing "Stock symbol suggestions" 75 | (let [ss (get-symbol-suggestion "google")] 76 | (is (> (count ss) 8)) 77 | (is (some (fn [s] (= (:exchDisp s) "NASDAQ")) ss))))) 78 | -------------------------------------------------------------------------------- /src/stockings/utils.clj: -------------------------------------------------------------------------------- 1 | (ns stockings.utils 2 | "Helper functions to access data through the YQL Web Service 3 | (Yahoo! Query Language) and parse a variety of value types." 4 | {:author "Filippo Tampieri "} 5 | (:use [clojure.string :only (join lower-case)] 6 | [clojure.contrib.def :only (defvar defvar-)] 7 | [clojure.contrib.json :only (read-json)]) 8 | (:require [clj-http.client :as client]) 9 | (:import (org.joda.time DateTime LocalDate) 10 | (org.joda.time.format DateTimeFormat))) 11 | 12 | (defvar- yql-base-url "http://query.yahooapis.com/v1/public/yql") 13 | 14 | (defn yql-string 15 | "String values in a YQL query must be enclosed in double quotes. 16 | This function takes a value and returns it as a string wrapped in 17 | escaped double quotes." 18 | [v] 19 | (str "\"" v "\"")) 20 | 21 | (defn yql-string-list 22 | "A list of string values in a YQL query is a comma-separated list 23 | of strings, enclosed in parenthesis. Also, each string is wrapped 24 | in double quotes. This function takes a sequence of values and 25 | converts them in a list suitable for a YQL query." 26 | [vs] 27 | (str "(" (join "," (map yql-string vs)) ")")) 28 | 29 | (defn- strip-wrapper [^String s] 30 | (subs s 8 (dec (count s)))) 31 | 32 | (defn submit-yql-query 33 | "Takes a YQL query string and submit it to Yahoo!'s YQL service, 34 | returning the value of the :result key in the JSON body of the 35 | response as a Clojure map. It throws an Exception if the 36 | response status is anything other than 200 or if the JSON response 37 | indicates an error (the error description is included in the 38 | exception)." 39 | [^String query] 40 | (let [params {:q query 41 | :format "json" 42 | :env "http://datatables.org/alltables.env" 43 | :callback "wrapper" ; wrap JSON response so errors are 44 | ; returned in payload rather than 45 | ; throw exceptions 46 | } 47 | response (client/get yql-base-url {:query-params params}) 48 | status (:status response)] 49 | (if (not= status 200) 50 | (throw (Exception. (str status)))) 51 | (let [payload (-> response :body strip-wrapper read-json)] 52 | (if-let [error (:error payload)] 53 | (throw (Exception. (:description error)))) 54 | (:results (:query payload))))) 55 | 56 | (defn map-parser 57 | "Takes a parser and a data value. If the data is a vector, it maps the 58 | parser to each item in the vector; otherwise, it simply calls the parser 59 | on the data value. This function is useful to process the result 60 | returned by `submit-yql-query` because YQL returns either a JSON object 61 | or a vector depending on whether the query returns one or more results." 62 | [parser data] 63 | (if (vector? data) 64 | (map parser data) 65 | (list (parser data)))) 66 | 67 | ;;; 68 | ;;; Parsers 69 | ;;; 70 | 71 | (defn parse-keyword 72 | "If the supplied string is not empty, it is returned as a lower-case 73 | keyword; otherwise it returns `nil`." 74 | [^String s] 75 | (if-not (empty? s) 76 | (keyword (lower-case s)))) 77 | 78 | (defn parse-int 79 | "If the supplied string represents a valid integer, it returns its 80 | value as an int; otherwise it returns `nil`." 81 | [^String s] 82 | (if s 83 | (if-let [m (re-matches #"(?:\+|\-)?\d+" s)] 84 | (Integer/parseInt m 10)))) 85 | 86 | (defn parse-long 87 | "If the supplied string represents a valid integer, it returns its 88 | value as a long; otherwise it returns `nil`." 89 | [^String s] 90 | (if s 91 | (if-let [m (re-matches #"(?:\+|\-)?\d+" s)] 92 | (Long/parseLong m 10)))) 93 | 94 | (defvar- multipliers 95 | {"B" 1.0e9 96 | "M" 1.0e6 97 | "K" 1.0e3}) 98 | 99 | (defn parse-double 100 | "If the supplied string represents a valid number in standard decimal 101 | notation (e.g. 123.4 and not 1.234E2), it returns its value as a 102 | double; otherwise it returns `nil`. The string can optionally end with 103 | a letter K, M, or B indicating thousands, millions, or billions 104 | respectively." 105 | [^String s] 106 | (if s 107 | (if-let [m (re-matches #"((?:\+|\-)?\d+(?:\.\d*)?)(B|K|M)?" s)] 108 | (* (Double/parseDouble (nth m 1)) (get multipliers (nth m 2) 1.0))))) 109 | 110 | (defn parse-percent 111 | "If the supplied string represents a valid percentage in standard 112 | decimal notation and ending with a % sign (e.g. 12.3%), it returns 113 | its fractional value as a double (e.g. 12.3% becomes 0.123); 114 | otherwise, it returns `nil`." 115 | [^String s] 116 | (if s 117 | (if-let [m (re-matches #"((?:\+|\-)?\d+(?:\.\d*)?)%" s)] 118 | (/ (Double/parseDouble (second m)) 100.0)))) 119 | 120 | (defvar- date-parsers 121 | [(DateTimeFormat/forPattern "yyyy-MM-dd") 122 | (DateTimeFormat/forPattern "M/dd/yyyy")]) 123 | 124 | (defn parse-date 125 | "If the supplied string represents a valid date in either of the 126 | yyyy-MM-dd or M/dd/yyyy formats, it returns the date as an 127 | `org.joda.time.LocalDate` object; otherwise, it returns `nil`." 128 | [^String s] 129 | (if s 130 | (first 131 | (for [parser date-parsers 132 | :let [d (try (.parseDateTime parser s) (catch Exception _ nil))] 133 | :when d] 134 | (.toLocalDate d))))) 135 | 136 | (defvar- time-parser (DateTimeFormat/forPattern "hh:mmaa")) 137 | 138 | (defn parse-time 139 | "If the supplied string represents a valid time in the hh:mmaa format 140 | (e.g. 9:30pm), it returns the time as an `org.joda.time.LocalTime` object; 141 | otherwise, it returns `nil`." 142 | [^String s] 143 | (if s 144 | (.toLocalTime (.parseDateTime time-parser s)))) 145 | 146 | -------------------------------------------------------------------------------- /src/stockings/exchanges.clj: -------------------------------------------------------------------------------- 1 | (ns stockings.exchanges 2 | "Functions to get a listing of the companies traded on the AMEX, 3 | NASDAQ, and NYSE stock exchanges. Based on the data made available 4 | by the NASDAQ at ." 5 | {:author "Filippo Tampieri "} 6 | (:use [clojure.string :only (split lower-case upper-case)] 7 | [clojure.contrib.def :only (defvar defvar-)] 8 | [clojure-csv.core :only (parse-csv)] 9 | [stockings.core :only (explode-stock-symbol)]) 10 | (:require [clj-http.client :as client])) 11 | 12 | ;;; 13 | ;;; Stock Exchanges 14 | ;;; 15 | 16 | (defvar nasdaq 17 | {:name "NASDAQ Stock Market", :symbol "NASDAQ"} 18 | "A map describing the NASDAQ Stock Market (NASDAQ).") 19 | 20 | (defvar nyse 21 | {:name "New York Stock Exchange", :symbol "NYSE"} 22 | "A map describing the New York Stock Exchange (NYSE).") 23 | 24 | (defvar amex 25 | {:name "NYSE Amex Equities", :symbol "AMEX"} 26 | "A map describing the NYSE Amex Equities (AMEX).") 27 | 28 | (defvar exchanges 29 | {:amex amex, 30 | :nasdaq nasdaq 31 | :nyse nyse} 32 | "A map from stock exchange keywords to stock exchange info maps.") 33 | 34 | ;;; 35 | ;;; Industry Sectors 36 | ;;; 37 | 38 | (defvar industry-sectors 39 | ["Basic Industries" 40 | "Capital Goods" 41 | "Consumer Durables" 42 | "Consumer Non-Durables" 43 | "Consumer Services" 44 | "Energy" 45 | "Finance" 46 | "Healthcare" 47 | "Miscellaneous" 48 | "Public Utilities" 49 | "Technology" 50 | "Transportation"] 51 | "A list of the major industry sectors as classified by the NASDAQ.") 52 | 53 | ;;; 54 | ;;; Companies 55 | ;;; 56 | 57 | (defvar- source-url "http://www.nasdaq.com/screening/companies-by-name.aspx") 58 | 59 | ;; Use a record instead of a map for efficiency since the lists of 60 | ;; companies are pretty long. 61 | (defrecord Company [exchange symbol name ipo-year sector industry]) 62 | 63 | (defn- valid-record? 64 | "A predicate that returns true if the given CSV record is well formed. 65 | The test is very basic and mainly serves to eliminate blank lines and 66 | the ending comma (which causes the CSV parser to return an extra record) 67 | from the parsed CSV file." 68 | [r] 69 | (and 70 | (vector? r) 71 | (= 9 (count r)) 72 | (< 0 (count (first r))))) 73 | 74 | (defn- convert-record 75 | "Transforms a CSV record into a Company record. 76 | It assumes the CSV record is a sequence of strings corresponding to the 77 | following fields: symbol, name, last sale, market capitalization, IPO year, 78 | sector, industry, URL for summary quote. 79 | The resulting Company record retains the exchange, symbol, name, IPO year, 80 | sector, and industry for the company." 81 | [exchange-key r] 82 | (let [stock-symbol (upper-case (nth r 0)) 83 | name (nth r 1) 84 | ipo-year (let [field (nth r 4)] 85 | (if (re-find #"^\d{4}$" field) (Integer/parseInt field 10))) 86 | sector (nth r 5) 87 | industry (nth r 6)] 88 | (Company. exchange-key stock-symbol name ipo-year sector industry))) 89 | 90 | (defn parse-companies 91 | "Parses a string of CSV-encoded companies and returns them 92 | as a sequence of Company records. It expects one company record per 93 | line with the following fields: symbol, name, last sale, market 94 | capitalization, IPO year, sector, industry, and URL for summary quote. 95 | The first line is expected to contain the column headers and is discarded. 96 | The first parameter should be a key representing the exchange on which all 97 | the companies described in the input string are traded. 98 | The result includes exchange, symbol name, IPO year, sector, and 99 | industry for each company." 100 | [exchange-key ^String s] 101 | (->> s 102 | parse-csv 103 | rest 104 | (filter valid-record?) 105 | (map (partial convert-record exchange-key)))) 106 | 107 | (defn get-companies 108 | "Requests a list of the companies traded on the stock exchange denoted by 109 | the supplied keyword. If no keyword is provided, it returns a merged list 110 | of the companies traded on the NASDAQ, NYSE, and AMEX exchanges. 111 | The companies are returned as a sequence of Company records. 112 | See `parse-companies` for details." 113 | ([exchange-key] 114 | (let [params {:render "download", :exchange (name exchange-key)} 115 | request (client/get source-url {:query-params params})] 116 | (parse-companies exchange-key (:body request)))) 117 | ([] 118 | (mapcat (fn [exchange-key] (get-companies exchange-key)) (keys exchanges)))) 119 | 120 | (defn- build-companies-map [companies] 121 | (letfn [(merge-entry [m c] 122 | (let [k (:symbol c)] 123 | (if-let [v (get m k)] 124 | (assoc m k (if (vector? v) (conj v c) [vector v c])) 125 | (assoc m k c))))] 126 | (reduce merge-entry {} companies))) 127 | 128 | (defn- normalize-stock-symbol [^String stock-symbol] 129 | (let [[exchange-symbol stock-symbol] (explode-stock-symbol stock-symbol)] 130 | [(if exchange-symbol (keyword (lower-case exchange-symbol))) 131 | (upper-case stock-symbol)])) 132 | 133 | (defn build-lookup 134 | "Builds a lookup function corresponding to the supplied list of companies 135 | (a sequence of Company records). The lookup function takes a stock symbol 136 | and returns the Company record corresponding to it. The stock symbol may 137 | optionally be prefixed with a stock exchange (e.g. \"NASDAQ:GOOG\"). 138 | If the stock exchange is not specified and multiple matches are found, it 139 | returns a vector of the matches; if no matches are found, it returns `nil`." 140 | [companies] 141 | (let [m (build-companies-map companies)] 142 | (fn [^String stock-symbol] 143 | (let [[exchange-key stock-symbol] (normalize-stock-symbol stock-symbol) 144 | res (get m stock-symbol)] 145 | (if exchange-key 146 | (if (vector? res) 147 | (first (filter (fn [c] (= exchange-key (:exchange c))) res)) 148 | (if (= exchange-key (:exchange res)) res)) 149 | res))))) 150 | 151 | -------------------------------------------------------------------------------- /src/stockings/alt.clj: -------------------------------------------------------------------------------- 1 | (ns stockings.alt 2 | "Alternative functions for getting current and historical stock quotes 3 | from Google Finance." 4 | {:author "Filippo Tampieri "} 5 | (:use [clojure.string :only (join split-lines)] 6 | [clojure.contrib.def :only (defvar-)] 7 | [stockings.utils :only (parse-double parse-int parse-long parse-keyword)] 8 | [stockings.core :only (bare-stock-symbol)]) 9 | (:require [clojure.xml :as xml] 10 | [clj-http.client :as client]) 11 | (:import (java.net URLEncoder) 12 | (java.io ByteArrayInputStream) 13 | (org.joda.time DateTime LocalDate DateTimeZone) 14 | (org.joda.time.format DateTimeFormat) 15 | (stockings.core HistoricalQuote))) 16 | 17 | ;;; 18 | ;;; Get current quotes 19 | ;;; 20 | 21 | (defvar- date-time-parser 22 | (.withZone (DateTimeFormat/forPattern "yyyyMMddHHmmss") DateTimeZone/UTC)) 23 | 24 | (defn- parse-date-time 25 | [^String date ^String time] 26 | (if-not (or (empty? date) (empty? time)) 27 | (.parseDateTime date-time-parser (str date time)))) 28 | 29 | (defn- parse-quote [raw-quote] 30 | (let [raw-map (apply hash-map 31 | (mapcat (fn [m] [(:tag m) (:data (:attrs m))]) 32 | (:content raw-quote)))] 33 | ;; If the requested stock symbol could not be found, the 34 | ;; value of the :company key will be empty 35 | ;; (in this case, the parser returns nil). 36 | (if-not (empty? (:company raw-map)) 37 | {:symbol (:symbol raw-map) 38 | :exchange (:exchange raw-map) 39 | :name (:company raw-map) 40 | :currency (parse-keyword (:currency raw-map)) 41 | :previous-close (parse-double (:y_close raw-map)) 42 | :open (parse-double (:open raw-map)) 43 | :low (parse-double (:low raw-map)) 44 | :high (parse-double (:high raw-map)) 45 | :last (parse-double (:last raw-map)) 46 | :last-date-time (parse-date-time (:trade_date_utc raw-map) 47 | (:trade_time_utc raw-map)) 48 | :change (parse-double (:change raw-map)) 49 | :percent-change (/ (parse-double (:perc_change raw-map)) 100.0) 50 | :volume (parse-long (:volume raw-map)) 51 | :avg-volume (parse-double (:avg_volume raw-map))}))) 52 | 53 | ;;; Note that we strip any exchange prefix from the stock symbol 54 | ;;; because the Google service does not recognize them. 55 | (defn- build-quotes-query-string [stock-symbols] 56 | (letfn [(to-param [s] 57 | (str "stock=" (URLEncoder/encode (bare-stock-symbol s) 58 | "UTF-8")))] 59 | (join "&" (map to-param stock-symbols)))) 60 | 61 | (defn get-quotes 62 | "Returns a sequence of stock quotes corresponding to the given stock 63 | symbols. A stock quote is a map with :symbol, :exchange, :name, 64 | :currency, :previous-close, :open, :low, :high, :last (trade), 65 | :last-date-time, :change, :percent-change, :volume, and :avg-volume 66 | keys. If data for a symbol cannot be found, its place in the result 67 | sequence will be `nil`." 68 | [& stock-symbols] 69 | (if stock-symbols 70 | (let [url (str "http://www.google.com/ig/api?" 71 | (build-quotes-query-string stock-symbols)) 72 | response (client/get url) 73 | status (:status response)] 74 | (if (not= status 200) 75 | (throw (Exception. (str status)))) 76 | (let [input-stream (ByteArrayInputStream. (.getBytes (:body response) 77 | "UTF-8")) 78 | payload (xml/parse input-stream)] 79 | (map parse-quote (:content payload)))))) 80 | 81 | (defn get-quote 82 | "Returns the current stock quote corresponding to the supplied stock 83 | symbol. The result is a map with :symbol, :exchange, :name, 84 | :currency, :previous-close, :open, :low, :high, :last (trade), 85 | :last-date-time, :change, :percent-change, :volume, and :avg-volume 86 | keys. If data for the supplied stock symbol cannot be found, it 87 | returns `nil`." 88 | [stock-symbol] 89 | (first (get-quotes stock-symbol))) 90 | 91 | ;;; 92 | ;;; Get historical quotes 93 | ;;; 94 | 95 | (defvar- date-parser (DateTimeFormat/forPattern "dd-MMM-yy")) 96 | 97 | (defn- parse-date 98 | "Parse a string representing a date into a `org.joda.time.LocalDate` object." 99 | [^String s] 100 | (.toLocalDate (.parseDateTime date-parser s))) 101 | 102 | (defvar- re-line 103 | #"((?:[0-9]|[123][0-9])-\w{3}-[0-9]{2}),([0-9]+(?:\.[0-9]*)?),([0-9]+(?:\.[0-9]*)?),([0-9]+(?:\.[0-9]*)?),([0-9]+(?:\.[0-9]*)?),([0-9]+(?:\.[0-9]*)?)" 104 | "The regular expression used to match one line in the CSV-encoded quotes. 105 | It matches a line in the form `Date,Open,High,Low,Close,Volume` 106 | where the `Date` is given as dd-MMM-yy and the other fields are 107 | non-negative numbers with an optional fractional part.") 108 | 109 | (defn- valid-record? 110 | "A predicate that validates whether the regular expression matches 111 | produced by one line of the CSV-encoded quotes were successful. 112 | The test is very basic and only checks that all the expected capturing 113 | groups have matches." 114 | [r] 115 | (and r 116 | (= 7 (count r)))) 117 | 118 | (defn- convert-record 119 | "Converts the regular expression matches corresponding to one line of 120 | the CSV-encoded quotes into a HistoricalQuote record." 121 | [r] 122 | (let [date (parse-date (nth r 1)) 123 | open (Double/parseDouble (nth r 2)) 124 | high (Double/parseDouble (nth r 3)) 125 | low (Double/parseDouble (nth r 4)) 126 | close (Double/parseDouble (nth r 5)) 127 | volume (Double/parseDouble (nth r 6))] 128 | (HistoricalQuote. date open high low close volume))) 129 | 130 | (defn parse-historical-quotes 131 | "Parses a string of CSV-encoded historical stock quotes and returns them 132 | as a sequence of `HistoricalQuote` records. It expects one quote per line 133 | with fields for date, open, high, low, close, and volume. The first line 134 | is assumed to be column headers and is discarded." 135 | [^String s] 136 | (->> s 137 | split-lines 138 | rest 139 | (map (partial re-matches re-line)) 140 | (filter valid-record?) 141 | (map convert-record))) 142 | 143 | (defn- get-historical-quotes* 144 | "Tries to get historical quotes from Google Finance. If the request fails 145 | with a 400 status code, it means the stock symbol could not be found or 146 | the dates were invalid; in this case, it just returns `nil`; otherwise, it 147 | rethrows the exception." 148 | [params] 149 | (try 150 | (client/get "http://www.google.com/finance/historical" 151 | {:query-params params}) 152 | (catch Exception e 153 | (let [status (parse-int (.getMessage e))] 154 | (if-not (= status 400) 155 | (throw e)))))) 156 | 157 | (defn get-historical-quotes 158 | "Returns a sequence of historical stock quotes corresponding to the 159 | supplied stock symbol, one quote per day between the supplied start 160 | and end dates (as `org.joda.time.LocalDate` objects). Quotes 161 | corresponding to dates falling on weekends and holidays are not 162 | included in the resulting sequence. If quotes for the given symbol 163 | or period cannot be found, it returns `nil`. The supplied stock symbol 164 | can have an optional exchange prefix (.e.g \"GOOG\" or \"NASDAQ:GOOG\"). 165 | 166 | **NOTE:** The Google Finance service limits the result to 4,000 items. 167 | If the supplied date range results in more than that limit, only the 168 | most recent 4,000 quotes will be returned." 169 | [^String stock-symbol ^LocalDate start-date ^LocalDate end-date] 170 | (let [params {:q stock-symbol 171 | :startdate (str start-date) 172 | :enddate (str end-date) 173 | :output "csv"} 174 | response (get-historical-quotes* params)] 175 | (when response 176 | (if (not= (:status response) 200) 177 | (throw (Exception. (str (:status response))))) 178 | (parse-historical-quotes (:body response))))) 179 | 180 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | stockings 2 | ========= 3 | 4 | Stockings is a Clojure library that gives you easy access to financial 5 | data such as current and historical stock quotes, current currency 6 | exchange rates, stock symbol suggestions, stock and company info by 7 | exchanges and industry sectors, and more. 8 | 9 | 10 | Usage 11 | ----- 12 | 13 | The stockings library is hosted on clojars.org. 14 | 15 | Add ``[com.fxtlabs/stockings "1.0.0"]`` to the dependencies 16 | list in your ``project.clj`` file and run ``lein deps`` to download the 17 | library from the Clojars archives. 18 | 19 | 20 | License 21 | ------- 22 | 23 | Copyright (C) 2011 Filippo Tampieri 24 | 25 | Distributed under the 26 | `Eclipse Public License v1.0 `_, 27 | the same as Clojure. 28 | 29 | 30 | Documentation 31 | ------------- 32 | 33 | Autodoc-generated API reference documentation can be found at 34 | http://stockings.fxtlabs.com. 35 | 36 | You can generate a local copy with ``lein autodoc``; it will be saved 37 | in the ``autodoc`` directory under the project root. 38 | 39 | 40 | Examples 41 | -------- 42 | 43 | Once you have the stockings JAR file on your classpath, you can run a 44 | REPL, load the library and start exploring. The following shows a few 45 | commands (see user>) and their output (formatted here for readability): 46 | 47 | :: 48 | 49 | user> (use 'stockings.core) 50 | nil 51 | 52 | user> (get-quote "YHOO") 53 | {:symbol "YHOO", 54 | :last 16.02, 55 | :last-date-time #, 56 | :name "Yahoo! Inc.", 57 | :low 15.95, 58 | :open 16.04, 59 | :previous-close 15.98, 60 | :high 16.19, 61 | :volume 20096766} 62 | 63 | gets the current stock quote for YAHOO! Note that the time for the 64 | last trade is represented by a ``org.joda.time.DateTime`` 65 | object in UTC. Stockings uses the Joda-Time library for all its 66 | time-related values. 67 | 68 | :: 69 | 70 | user> (get-quotes "GOOG" "NASDAQ:AAPL") 71 | ({:symbol "GOOG", ...} 72 | {:symbol "AAPL", ...}) 73 | 74 | 75 | gets the current stock quotes for Google and Apple. Note that stock 76 | symbols may optionally be prefixed with the name of an exchange (like 77 | ``NASDAQ:AAPL`` above); however, the Yahoo! Finance service providing the 78 | financial data does not recognize prefixed stock symbols, so the 79 | prefix is simply dropped. 80 | 81 | :: 82 | 83 | user> (import [org.joda.time LocalDate]) 84 | org.joda.time.LocalDate 85 | 86 | user> (get-historical-quotes "YHOO" (LocalDate. 2011 4 1) (LocalDate. 2011 5 1)) 87 | (#:stockings.core.HistoricalQuote{:date #, 88 | :open 17.46, 89 | :high 17.77, 90 | :low 17.36, 91 | :close 17.7, 92 | :volume 30800000} 93 | #:stockings.core.HistoricalQuote{:date #, 94 | :open 17.22, 95 | :high 17.53, 96 | :low 17.17, 97 | :close 17.51, 98 | :volume 14414700} 99 | ... 100 | #:stockings.core.HistoricalQuote{:date #, 101 | :open 16.83, 102 | :high 16.98, 103 | :low 16.72, 104 | :close 16.84, 105 | :volume 12487400}) 106 | 107 | gets the historical stock quotes for Yahoo! for the period starting on 108 | April 1, 2011 and ending on May 1, 2011. There are a few things worth 109 | noting: 110 | 111 | * The quotes are returned in chronological order starting from the 112 | most recent. 113 | * There are no entries for dates that fall on a weekend or holiday. 114 | * The dates are represented by ``org.joda.time.LocalDate`` objects. 115 | Note that class ``LocalDate`` does not describe an exact instant 116 | in time (e.g. an instant down to millisecond precision); instead, 117 | it just refers to a given calendar day and no particular instant 118 | of time within it. 119 | 120 | :: 121 | 122 | user> (get-stock "YHOO") 123 | {:symbol "YHOO", 124 | :name "Yahoo! Inc.", 125 | :start-date #, 126 | :end-date #, 127 | :sector "Technology", 128 | :industry "Internet Information Providers", 129 | :full-time-employees 13600} 130 | 131 | gets some info on a company. 132 | 133 | :: 134 | 135 | user> (get-industry-sectors) 136 | ({:name "Basic Materials", 137 | :industries [{:id "112", :name "Agricultural Chemicals"} 138 | {:id "132", :name "Aluminum"} 139 | {:id "110", :name "Chemicals - Major Diversified"} 140 | ... 141 | {:id "111", :name "Synthetics"}]} 142 | {:name "Conglomerates", 143 | :industries [{:id "210", :name "Conglomerates"}]} 144 | ...) 145 | 146 | gets a list of all industry sectors and a list of industries for each 147 | sector. 148 | 149 | :: 150 | 151 | user> (get-industry 112) 152 | {:id "112", 153 | :name "Agricultural Chemicals", 154 | :companies [{:name "Agrium Inc.", :symbol "AGU"} 155 | {:name "American Vanguard Corporation", :symbol "AVD"} 156 | ... 157 | {:name "Yongye International, Inc.", :symbol "YONG"}]} 158 | 159 | gets a list of all companies for a given industry (identified by its 160 | ID). 161 | 162 | :: 163 | 164 | user> (get-exchange-rate :usd :eur) 165 | {:base :usd, 166 | :quote :eur, 167 | :rate 0.7002, 168 | :ask 0.7003, 169 | :bid 0.7001, 170 | :date-time #} 171 | 172 | gets the current exchange rate from a base currency (USD) to a quote (or 173 | counter) currency (EUR). The currencies are denoted by their ISO 4217 174 | 3-letter designators used as strings or keywords. In other words: 175 | 176 | :: 177 | 178 | user> (get-exchange-rate "USD" "EUR") 179 | 180 | also works as above. 181 | 182 | :: 183 | 184 | user> (get-symbol-suggestion "Terra Nitro") 185 | [{:symbol "TNH", 186 | :name "Terra Nitrogen Company, L.P.", 187 | :exch "NYQ", 188 | :type "S", 189 | :exchDisp "NYSE", 190 | :typeDisp "Equity"}] 191 | 192 | gets stock symbol suggestion for the company whose name starts with 193 | the given prefix. Note that large companies may be traded on several 194 | exchanges and thus correspond to more than one symbol. 195 | 196 | So far so good. Now, let us look at a slightly more complex example. 197 | Yahoo! Finance actually offers a lot more financial data when asking 198 | for a stock quote. The ``get-quote`` example given at the beginning of 199 | this section only returned a small subset of this data. Let us now see 200 | how we can pick and choose what data we want to include in our stock 201 | quotes. 202 | 203 | :: 204 | 205 | user> raw-quote-keys 206 | (:symbol :Name :PERatio :EarningsShare :EPSEstimateNextYear 207 | :PercebtChangeFromYearHigh :ChangeFromTwoHundreddayMovingAverage 208 | :TwoHundreddayMovingAverage :ChangeinPercent :Bid :DaysLow ...) 209 | 210 | Var ``raw-quote-keys`` lists all the keys to the data contained in a raw 211 | stock quote (what we can get from Yahoo! Finance). Let's say we want 212 | to get custom stock quotes including the values of the keys ``:symbol``, 213 | ``:Name``, and ``:MarketCapitalization``. 214 | 215 | :: 216 | 217 | user> (def parser (build-quote-parser {:symbol :symbol 218 | :name :Name 219 | :cap :MarketCapitalization})) 220 | #'user/parser 221 | 222 | user> (get-quote parser "YHOO") 223 | {:symbol "YHOO", :name "Yahoo! Inc.", :cap 2.0873E10} 224 | 225 | Function ``get-quote`` takes a quote parser as an optional first 226 | parameter. The quote parser function is given the raw quote (with all 227 | the keys listed in ``raw-quote-keys``) and returns a parsed quote as a map 228 | with the requested keys (``:symbol``, ``:name``, and ``:cap`` in this case). All 229 | we had to do was call ``build-quote-parser`` with a map specifying the 230 | correspondences between the keys we want in the final quote and the 231 | keys in the raw quote. So ``get-quote`` can get us back a result as a map 232 | with exactly the info we want, no more, no less; note also that we are 233 | completely free to use whatever names we want for the keys of the 234 | resulting map. 235 | 236 | Should we require even more flexibility, we can write our own quote 237 | parser directly: 238 | 239 | :: 240 | 241 | user> (defrecord MyQuote [stock-symbol company-name last last-date-time]) 242 | user.MyQuote 243 | 244 | user> (defn my-parser [q] 245 | (let [stock-symbol (parse-quote-item q :symbol) 246 | company-name (parse-quote-item q :Name) 247 | last (parse-quote-item q :LastTradePriceOnly) 248 | last-date-time (parse-last-trade-date-time q)] 249 | (MyQuote. stock-symbol company-name last last-date-time))) 250 | #'user/my-parser 251 | 252 | user> (get-quote my-parser "YHOO") 253 | #:user.MyQuote{:stock-symbol "YHOO", 254 | :company-name "Yahoo! Inc.", 255 | :last 16.02, 256 | :last-date-time #} 257 | 258 | The quote parser does not have to return a map; it can actually return 259 | any type you like. Function ``parse-quote-item`` is used to parse one 260 | field of the raw quote; it knows the data type (string, double, int, 261 | etc.) of every field and will return the correct value and type. 262 | Function ``parse-last-trade-date-time`` combines the values of 263 | ``:LastTradeDate`` and ``:LastTradeTime`` and returns them as a 264 | ``org.joda.time.DateTime`` object in UTC. You will want to use this 265 | function if you need access to the last trade time and date because 266 | the data returned by Yahoo! Finance for these fields is a bit 267 | inconsistent; the date value is in UTC, but the time value is in the 268 | time zone of the North American East Coast! Function 269 | ``parse-last-trade-date-time`` corrects for this and returns a date time 270 | in UTC. 271 | 272 | Stockings includes even more data and functions to help you dig into it. 273 | Please, consult the API Reference Guide at 274 | http://stockings.fxtlabs.com for more details. 275 | 276 | 277 | Notes 278 | ----- 279 | 280 | Most of the financial data is downloaded from Yahoo! Finance using 281 | YQL, the Yahoo! Query Language to query the 282 | yahoo.finance.historicaldata, yahoo.finance.industry, 283 | yahoo.finance.quotes, yahoo.finance.sectors, yahoo.finance.stocks, and 284 | yahoo.finance.xchange data tables. 285 | 286 | Google Stock is used as an alternate data source for current and 287 | historical stock quotes if desired (see stockings.alt namespace). 288 | 289 | NASDAQ keeps lists of the companies traded on the NASDAQ, NYSE, and 290 | AMEX exchanges at http://www.nasdaq.com/screening/company-list.aspx. 291 | All the data in these lists can be accessed through the 292 | stockings.exchanges namespace. Note that the grouping of companies 293 | into industries and sectors used by NASDAQ does not match exactly 294 | those used by Yahoo! Finance, so you may find some discrepancies between 295 | the groupings you get through the stockings.exchanges and stockings.core 296 | namespaces. 297 | 298 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.fxtlabs/stockings "1.1.0-SNAPSHOT" 2 | :description "Easy access to financial data: stock quotes, exchange rates, industry sectors, companies, and more." 3 | :url "https://github.com/fxtlabs/stockings" 4 | :dependencies [[org.clojure/clojure "1.2.1"] 5 | [joda-time "1.6"] 6 | [clojure-csv "1.2.4"] 7 | [clj-http "0.1.3"]] 8 | :dev-dependencies 9 | [[com.fxtlabs/autodoc "0.8.0-SNAPSHOT" 10 | :exclusions [org.clojure/clojure org.clojure/clojure-contrib]]] 11 | :license {:name "Eclipse Public License - v 1.0" 12 | :url "http://www.eclipse.org/legal/epl-v10.html" 13 | :distribution :repo 14 | :comments "same as Clojure"} 15 | :autodoc 16 | {:name "Stockings" 17 | :copyright "Copyright 2011 Filippo Tampieri" 18 | :root "." 19 | :source-path "src" 20 | :web-src-dir "https://github.com/fxtlabs/stockings/blob/" 21 | :web-home "http://stockings.fxtlabs.com/" 22 | :output-path "autodoc" 23 | :param-dir "autodoc-params" 24 | :namespaces-to-document ["stockings"] 25 | :load-except-list [#"/test/" #"project.clj"] 26 | :description "Stockings is a Clojure library that gives you easy access to 27 | financial data such as current and historical stock quotes, current currency 28 | exchange rates, stock symbol suggestions, stock and company info by 29 | exchanges and industry sectors, and more. 30 | 31 | You can find Stockings' source code at . 32 | 33 | ## Usage 34 | 35 | The stockings library is available from Clojars at 36 | . 37 | 38 | If you are using Leiningen, just add `[com.fxtlabs/stockings \"1.0.0\"]` 39 | to the dependencies list in your `project.clj` file and run `lein deps` 40 | to download the library from the Clojars archives. 41 | 42 | ## License 43 | 44 | Copyright (C) 2011 Filippo Tampieri. 45 | 46 | Distributed under the [Eclipse Public License v1.0](), 47 | the same as Clojure. 48 | 49 | ## Examples 50 | 51 | Once you have the stockings JAR file on your classpath, you can run a 52 | REPL, load the library and start exploring. The following examples show a few 53 | commands (see user>) and their output (formatted here for readability). 54 | They all assume you are using the `stockings.core` namespace: 55 | 56 | user> (use 'stockings.core) 57 | nil 58 | 59 | ### Current stock quotes 60 | 61 | user> (get-quote \"YHOO\") 62 | {:symbol \"YHOO\", 63 | :last 16.02, 64 | :last-date-time #, 65 | :name \"Yahoo! Inc.\", 66 | :low 15.95, 67 | :open 16.04, 68 | :previous-close 15.98, 69 | :high 16.19, 70 | :volume 20096766} 71 | 72 | gets the current stock quote for YAHOO! Note that the time for the 73 | last trade is represented by a `org.joda.time.DateTime` 74 | object in UTC. Stockings uses the Joda-Time library for all its 75 | time-related values. 76 | 77 | user> (get-quotes \"GOOG\" \"NASDAQ:AAPL\") 78 | ({:symbol \"GOOG\", ...} 79 | {:symbol \"AAPL\", ...}) 80 | 81 | 82 | gets the current stock quotes for Google and Apple. Note that stock 83 | symbols may optionally be prefixed with the name of an exchange (like 84 | `NASDAQ:AAPL` above); however, the Yahoo! Finance service providing the 85 | financial data does not recognize prefixed stock symbols, so the 86 | prefix is simply dropped. 87 | 88 | ### Historical stock quotes 89 | 90 | user> (import [org.joda.time LocalDate]) 91 | org.joda.time.LocalDate 92 | 93 | user> (get-historical-quotes \"YHOO\" (LocalDate. 2011 4 1) (LocalDate. 2011 5 1)) 94 | (#:stockings.core.HistoricalQuote{:date #, 95 | :open 17.46, 96 | :high 17.77, 97 | :low 17.36, 98 | :close 17.7, 99 | :volume 30800000} 100 | #:stockings.core.HistoricalQuote{:date #, 101 | :open 17.22, 102 | :high 17.53, 103 | :low 17.17, 104 | :close 17.51, 105 | :volume 14414700} 106 | ... 107 | #:stockings.core.HistoricalQuote{:date #, 108 | :open 16.83, 109 | :high 16.98, 110 | :low 16.72, 111 | :close 16.84, 112 | :volume 12487400}) 113 | 114 | gets the historical stock quotes for Yahoo! for the period starting on 115 | April 1, 2011 and ending on May 1, 2011. There are a few things worth 116 | noting: 117 | 118 | * The quotes are returned in chronological order starting from the 119 | most recent. 120 | * There are no entries for dates that fall on a weekend or holiday. 121 | * The dates are represented by `org.joda.time.LocalDate` objects. 122 | Note that class ``LocalDate`` does not describe an exact instant 123 | in time (e.g. an instant down to millisecond precision); instead, 124 | it just refers to a given calendar day and no particular instant 125 | of time within it. 126 | 127 | ### Info on companies and industry sectors 128 | 129 | user> (get-stock \"YHOO\") 130 | {:symbol \"YHOO\", 131 | :name \"Yahoo! Inc.\", 132 | :start-date #, 133 | :end-date #, 134 | :sector \"Technology\", 135 | :industry \"Internet Information Providers\", 136 | :full-time-employees 13600} 137 | 138 | gets some info on a company. 139 | 140 | user> (get-industry-sectors) 141 | ({:name \"Basic Materials\", 142 | :industries [{:id \"112\", :name \"Agricultural Chemicals\"} 143 | {:id \"132\", :name \"Aluminum\"} 144 | {:id \"110\", :name \"Chemicals - Major Diversified\"} 145 | ... 146 | {:id \"111\", :name \"Synthetics\"}]} 147 | {:name \"Conglomerates\", 148 | :industries [{:id \"210\", :name \"Conglomerates\"}]} 149 | ...) 150 | 151 | gets a list of all industry sectors and a list of industries for each 152 | sector. 153 | 154 | user> (get-industry 112) 155 | {:id \"112\", 156 | :name \"Agricultural Chemicals\", 157 | :companies [{:name \"Agrium Inc.\", :symbol \"AGU\"} 158 | {:name \"American Vanguard Corporation\", :symbol \"AVD\"} 159 | ... 160 | {:name \"Yongye International, Inc.\", :symbol \"YONG\"}]} 161 | 162 | gets a list of all companies for a given industry (identified by its 163 | ID). 164 | 165 | ### Current currency exchange rates 166 | 167 | user> (get-exchange-rate :usd :eur) 168 | {:base :usd, 169 | :quote :eur, 170 | :rate 0.7002, 171 | :ask 0.7003, 172 | :bid 0.7001, 173 | :date-time #} 174 | 175 | gets the current exchange rate from a base currency (USD) to a quote (or 176 | counter) currency (EUR). The currencies are denoted by their ISO 4217 177 | 3-letter designators used as strings or keywords. In other words: 178 | 179 | user> (get-exchange-rate \"USD\" \"EUR\") 180 | 181 | also works as above. 182 | 183 | ### Stock symbol lookup 184 | 185 | user> (get-symbol-suggestion \"Terra Nitro\") 186 | [{:symbol \"TNH\", 187 | :name \"Terra Nitrogen Company, L.P.\", 188 | :exch \"NYQ\", 189 | :type \"S\", 190 | :exchDisp \"NYSE\", 191 | :typeDisp \"Equity\"}] 192 | 193 | gets stock symbol suggestion for the company whose name starts with 194 | the given prefix. Note that large companies may be traded on several 195 | exchanges and thus correspond to more than one symbol. 196 | 197 | ### Current stock quotes - advanced use 198 | 199 | So far so good. Now, let us look at a slightly more complex example. 200 | Yahoo! Finance actually offers a lot more financial data when asking 201 | for a stock quote. The `get-quote` example given at the beginning of 202 | this section only returned a small subset of this data. Let us now see 203 | how we can pick and choose what data we want to include in our stock 204 | quotes. 205 | 206 | user> raw-quote-keys 207 | (:symbol :Name :PERatio :EarningsShare :EPSEstimateNextYear 208 | :PercebtChangeFromYearHigh :ChangeFromTwoHundreddayMovingAverage 209 | :TwoHundreddayMovingAverage :ChangeinPercent :Bid :DaysLow ...) 210 | 211 | Var `raw-quote-keys` lists all the keys to the data contained in a raw 212 | stock quote (what we can get from Yahoo! Finance). Let's say we want 213 | to get custom stock quotes including the values of the keys `:symbol`, 214 | `:Name`, and `:MarketCapitalization`. 215 | 216 | user> (def parser (build-quote-parser {:symbol :symbol 217 | :name :Name 218 | :cap :MarketCapitalization})) 219 | #'user/parser 220 | 221 | user> (get-quote parser \"YHOO\") 222 | {:symbol \"YHOO\", :name \"Yahoo! Inc.\", :cap 2.0873E10} 223 | 224 | Function `get-quote` takes a quote parser as an optional first 225 | parameter. The quote parser function is given the raw quote (with all 226 | the keys listed in `raw-quote-keys`) and returns a parsed quote as a map 227 | with the requested keys (`:symbol`, `:name`, and `:cap` in this case). All 228 | we had to do was call `build-quote-parser` with a map specifying the 229 | correspondences between the keys we want in the final quote and the 230 | keys in the raw quote. So `get-quote` can get us back a result as a map 231 | with exactly the info we want, no more, no less; note also that we are 232 | completely free to use whatever names we want for the keys of the 233 | resulting map. 234 | 235 | Should we require even more flexibility, we can write our own quote 236 | parser directly: 237 | 238 | user> (defrecord MyQuote [stock-symbol company-name last last-date-time]) 239 | user.MyQuote 240 | 241 | user> (defn my-parser [q] 242 | (let [stock-symbol (parse-quote-item q :symbol) 243 | company-name (parse-quote-item q :Name) 244 | last (parse-quote-item q :LastTradePriceOnly) 245 | last-date-time (parse-last-trade-date-time q)] 246 | (MyQuote. stock-symbol company-name last last-date-time))) 247 | #'user/my-parser 248 | 249 | user> (get-quote my-parser \"YHOO\") 250 | #:user.MyQuote{:stock-symbol \"YHOO\", 251 | :company-name \"Yahoo! Inc.\", 252 | :last 16.02, 253 | :last-date-time #} 254 | 255 | The quote parser does not have to return a map; it can actually return 256 | any type you like. Function `parse-quote-item` is used to parse one 257 | field of the raw quote; it knows the data type (string, double, int, 258 | etc.) of every field and will return the correct value and type. 259 | Function `parse-last-trade-date-time` combines the values of 260 | `:LastTradeDate` and `:LastTradeTime` and returns them as a 261 | `org.joda.time.DateTime` object in UTC. You will want to use this 262 | function if you need access to the last trade time and date because 263 | the data returned by Yahoo! Finance for these fields is a bit 264 | inconsistent; the date value is in UTC, but the time value is in the 265 | time zone of the North American East Coast! Function 266 | `parse-last-trade-date-time` corrects for this and returns a date time 267 | in UTC. 268 | 269 | ## Digging Deeper 270 | 271 | Stockings includes even more data and functions to help you dig into it. 272 | Please, consult the detailed API documentation below to find out more."}) 273 | 274 | -------------------------------------------------------------------------------- /LICENSE.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Eclipse Public License - Version 1.0 8 | 25 | 26 | 27 | 28 | 29 | 30 |

Eclipse Public License - v 1.0

31 | 32 |

THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 33 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR 34 | DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS 35 | AGREEMENT.

36 | 37 |

1. DEFINITIONS

38 | 39 |

"Contribution" means:

40 | 41 |

a) in the case of the initial Contributor, the initial 42 | code and documentation distributed under this Agreement, and

43 |

b) in the case of each subsequent Contributor:

44 |

i) changes to the Program, and

45 |

ii) additions to the Program;

46 |

where such changes and/or additions to the Program 47 | originate from and are distributed by that particular Contributor. A 48 | Contribution 'originates' from a Contributor if it was added to the 49 | Program by such Contributor itself or anyone acting on such 50 | Contributor's behalf. Contributions do not include additions to the 51 | Program which: (i) are separate modules of software distributed in 52 | conjunction with the Program under their own license agreement, and (ii) 53 | are not derivative works of the Program.

54 | 55 |

"Contributor" means any person or entity that distributes 56 | the Program.

57 | 58 |

"Licensed Patents" mean patent claims licensable by a 59 | Contributor which are necessarily infringed by the use or sale of its 60 | Contribution alone or when combined with the Program.

61 | 62 |

"Program" means the Contributions distributed in accordance 63 | with this Agreement.

64 | 65 |

"Recipient" means anyone who receives the Program under 66 | this Agreement, including all Contributors.

67 | 68 |

2. GRANT OF RIGHTS

69 | 70 |

a) Subject to the terms of this Agreement, each 71 | Contributor hereby grants Recipient a non-exclusive, worldwide, 72 | royalty-free copyright license to reproduce, prepare derivative works 73 | of, publicly display, publicly perform, distribute and sublicense the 74 | Contribution of such Contributor, if any, and such derivative works, in 75 | source code and object code form.

76 | 77 |

b) Subject to the terms of this Agreement, each 78 | Contributor hereby grants Recipient a non-exclusive, worldwide, 79 | royalty-free patent license under Licensed Patents to make, use, sell, 80 | offer to sell, import and otherwise transfer the Contribution of such 81 | Contributor, if any, in source code and object code form. This patent 82 | license shall apply to the combination of the Contribution and the 83 | Program if, at the time the Contribution is added by the Contributor, 84 | such addition of the Contribution causes such combination to be covered 85 | by the Licensed Patents. The patent license shall not apply to any other 86 | combinations which include the Contribution. No hardware per se is 87 | licensed hereunder.

88 | 89 |

c) Recipient understands that although each Contributor 90 | grants the licenses to its Contributions set forth herein, no assurances 91 | are provided by any Contributor that the Program does not infringe the 92 | patent or other intellectual property rights of any other entity. Each 93 | Contributor disclaims any liability to Recipient for claims brought by 94 | any other entity based on infringement of intellectual property rights 95 | or otherwise. As a condition to exercising the rights and licenses 96 | granted hereunder, each Recipient hereby assumes sole responsibility to 97 | secure any other intellectual property rights needed, if any. For 98 | example, if a third party patent license is required to allow Recipient 99 | to distribute the Program, it is Recipient's responsibility to acquire 100 | that license before distributing the Program.

101 | 102 |

d) Each Contributor represents that to its knowledge it 103 | has sufficient copyright rights in its Contribution, if any, to grant 104 | the copyright license set forth in this Agreement.

105 | 106 |

3. REQUIREMENTS

107 | 108 |

A Contributor may choose to distribute the Program in object code 109 | form under its own license agreement, provided that:

110 | 111 |

a) it complies with the terms and conditions of this 112 | Agreement; and

113 | 114 |

b) its license agreement:

115 | 116 |

i) effectively disclaims on behalf of all Contributors 117 | all warranties and conditions, express and implied, including warranties 118 | or conditions of title and non-infringement, and implied warranties or 119 | conditions of merchantability and fitness for a particular purpose;

120 | 121 |

ii) effectively excludes on behalf of all Contributors 122 | all liability for damages, including direct, indirect, special, 123 | incidental and consequential damages, such as lost profits;

124 | 125 |

iii) states that any provisions which differ from this 126 | Agreement are offered by that Contributor alone and not by any other 127 | party; and

128 | 129 |

iv) states that source code for the Program is available 130 | from such Contributor, and informs licensees how to obtain it in a 131 | reasonable manner on or through a medium customarily used for software 132 | exchange.

133 | 134 |

When the Program is made available in source code form:

135 | 136 |

a) it must be made available under this Agreement; and

137 | 138 |

b) a copy of this Agreement must be included with each 139 | copy of the Program.

140 | 141 |

Contributors may not remove or alter any copyright notices contained 142 | within the Program.

143 | 144 |

Each Contributor must identify itself as the originator of its 145 | Contribution, if any, in a manner that reasonably allows subsequent 146 | Recipients to identify the originator of the Contribution.

147 | 148 |

4. COMMERCIAL DISTRIBUTION

149 | 150 |

Commercial distributors of software may accept certain 151 | responsibilities with respect to end users, business partners and the 152 | like. While this license is intended to facilitate the commercial use of 153 | the Program, the Contributor who includes the Program in a commercial 154 | product offering should do so in a manner which does not create 155 | potential liability for other Contributors. Therefore, if a Contributor 156 | includes the Program in a commercial product offering, such Contributor 157 | ("Commercial Contributor") hereby agrees to defend and 158 | indemnify every other Contributor ("Indemnified Contributor") 159 | against any losses, damages and costs (collectively "Losses") 160 | arising from claims, lawsuits and other legal actions brought by a third 161 | party against the Indemnified Contributor to the extent caused by the 162 | acts or omissions of such Commercial Contributor in connection with its 163 | distribution of the Program in a commercial product offering. The 164 | obligations in this section do not apply to any claims or Losses 165 | relating to any actual or alleged intellectual property infringement. In 166 | order to qualify, an Indemnified Contributor must: a) promptly notify 167 | the Commercial Contributor in writing of such claim, and b) allow the 168 | Commercial Contributor to control, and cooperate with the Commercial 169 | Contributor in, the defense and any related settlement negotiations. The 170 | Indemnified Contributor may participate in any such claim at its own 171 | expense.

172 | 173 |

For example, a Contributor might include the Program in a commercial 174 | product offering, Product X. That Contributor is then a Commercial 175 | Contributor. If that Commercial Contributor then makes performance 176 | claims, or offers warranties related to Product X, those performance 177 | claims and warranties are such Commercial Contributor's responsibility 178 | alone. Under this section, the Commercial Contributor would have to 179 | defend claims against the other Contributors related to those 180 | performance claims and warranties, and if a court requires any other 181 | Contributor to pay any damages as a result, the Commercial Contributor 182 | must pay those damages.

183 | 184 |

5. NO WARRANTY

185 | 186 |

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS 187 | PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 188 | OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, 189 | ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY 190 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely 191 | responsible for determining the appropriateness of using and 192 | distributing the Program and assumes all risks associated with its 193 | exercise of rights under this Agreement , including but not limited to 194 | the risks and costs of program errors, compliance with applicable laws, 195 | damage to or loss of data, programs or equipment, and unavailability or 196 | interruption of operations.

197 | 198 |

6. DISCLAIMER OF LIABILITY

199 | 200 |

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT 201 | NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, 202 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING 203 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF 204 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 205 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR 206 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED 207 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

208 | 209 |

7. GENERAL

210 | 211 |

If any provision of this Agreement is invalid or unenforceable under 212 | applicable law, it shall not affect the validity or enforceability of 213 | the remainder of the terms of this Agreement, and without further action 214 | by the parties hereto, such provision shall be reformed to the minimum 215 | extent necessary to make such provision valid and enforceable.

216 | 217 |

If Recipient institutes patent litigation against any entity 218 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 219 | Program itself (excluding combinations of the Program with other 220 | software or hardware) infringes such Recipient's patent(s), then such 221 | Recipient's rights granted under Section 2(b) shall terminate as of the 222 | date such litigation is filed.

223 | 224 |

All Recipient's rights under this Agreement shall terminate if it 225 | fails to comply with any of the material terms or conditions of this 226 | Agreement and does not cure such failure in a reasonable period of time 227 | after becoming aware of such noncompliance. If all Recipient's rights 228 | under this Agreement terminate, Recipient agrees to cease use and 229 | distribution of the Program as soon as reasonably practicable. However, 230 | Recipient's obligations under this Agreement and any licenses granted by 231 | Recipient relating to the Program shall continue and survive.

232 | 233 |

Everyone is permitted to copy and distribute copies of this 234 | Agreement, but in order to avoid inconsistency the Agreement is 235 | copyrighted and may only be modified in the following manner. The 236 | Agreement Steward reserves the right to publish new versions (including 237 | revisions) of this Agreement from time to time. No one other than the 238 | Agreement Steward has the right to modify this Agreement. The Eclipse 239 | Foundation is the initial Agreement Steward. The Eclipse Foundation may 240 | assign the responsibility to serve as the Agreement Steward to a 241 | suitable separate entity. Each new version of the Agreement will be 242 | given a distinguishing version number. The Program (including 243 | Contributions) may always be distributed subject to the version of the 244 | Agreement under which it was received. In addition, after a new version 245 | of the Agreement is published, Contributor may elect to distribute the 246 | Program (including its Contributions) under the new version. Except as 247 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives no 248 | rights or licenses to the intellectual property of any Contributor under 249 | this Agreement, whether expressly, by implication, estoppel or 250 | otherwise. All rights in the Program not expressly granted under this 251 | Agreement are reserved.

252 | 253 |

This Agreement is governed by the laws of the State of New York and 254 | the intellectual property laws of the United States of America. No party 255 | to this Agreement will bring a legal action under this Agreement more 256 | than one year after the cause of action arose. Each party waives its 257 | rights to a jury trial in any resulting litigation.

258 | 259 | 260 | 261 | 262 | -------------------------------------------------------------------------------- /src/stockings/core.clj: -------------------------------------------------------------------------------- 1 | (ns stockings.core 2 | "Get current and historical stock quotes, current currency exchange rates, 3 | and info on industry sectors, industries, companies, and stocks 4 | from Yahoo! Finance." 5 | {:author "Filippo Tampieri "} 6 | (:use [clojure.string :only (split join lower-case)] 7 | [clojure.contrib.def :only (defvar defvar-)] 8 | [clojure.contrib.json :only (read-json)]) 9 | (:require [clj-http.client :as client] 10 | [stockings.utils :as yql]) 11 | (:import (org.joda.time LocalDate LocalTime DateTimeZone) 12 | (java.util TimeZone))) 13 | 14 | ;;; NOTES 15 | 16 | ;;; Yahoo! Finance web services do not recognize stock symbols prefixed with 17 | ;;; a stock exchange, so we always strip it before passing the symbols 18 | ;;; to the web queries. 19 | 20 | ;;; 21 | ;;; Utilities 22 | ;;; 23 | 24 | (defn prefix-stock-symbol 25 | "Takes the symbols for a stock exchange and a stock and combines them 26 | to create an exchange-prefixed stock symbol (e.g. \"NASDAQ\" and 27 | \"YHOO\" are combined into \"NASDAQ:YHOO\")." 28 | [^String exchange-symbol ^String stock-symbol] 29 | (if exchange-symbol 30 | (str exchange-symbol ":" stock-symbol) 31 | stock-symbol)) 32 | 33 | (defn explode-stock-symbol 34 | "Takes a stock symbol and explodes it into its exchange prefix and 35 | bare symbol parts, returning a vector with two entries. If the 36 | exchange prefix is missing, the first entry in the vector will be 37 | `nil`." 38 | [^String stock-symbol] 39 | (let [[s exchange symbol] (re-matches #"(.+):(.+)" stock-symbol)] 40 | (if s 41 | [exchange symbol] 42 | [nil stock-symbol]))) 43 | 44 | (defn bare-stock-symbol 45 | "Returns a stock symbol stripped of the stock exchange prefix, if any." 46 | [^String stock-symbol] 47 | (second (explode-stock-symbol stock-symbol))) 48 | 49 | (defn get-largest-lte 50 | "Returns the value mapped to the largest key that is less than or 51 | equal to the supplied key; returns `not-found` or `nil` if a matching key 52 | cannot be found. The supplied map should be a sorted map. You can use 53 | this function to lookup historical stock quotes by date and still get 54 | a valid quote when the date falls on a weekend or holiday. Use 55 | `to-sorted-map` to create a sorted map from a sequence of historical quotes." 56 | ([^clojure.lang.Sorted map key] (get-largest-lte map key nil)) 57 | ([^clojure.lang.Sorted map key not-found] 58 | (if-let [lte-part (rsubseq map <= key)] 59 | (val (first lte-part)) 60 | not-found))) 61 | 62 | (defn to-sorted-map 63 | "Builds a sorted map from the supplied collection. The supplied `get-key` 64 | function is used to compute the key corresponding to a given item 65 | in the collection." 66 | [get-key xs] 67 | (apply sorted-map (mapcat (fn [x] [(get-key x) x]) xs))) 68 | 69 | ;;; NOTE: the date and time values returned by queries that are 70 | ;;; resolved to http://download.yahoo.finance.com/d/quotes.csv are 71 | ;;; wrong. It seems that the date is given for the UTC zone, but the 72 | ;;; time is given for the America/New_York time zone. 73 | ;;; The following code addresses the problem and assembles a correct 74 | ;;; DateTime object in UTC from the date and time given. 75 | ;;; You will want to use it if you are assembling a time stamp from 76 | ;;; the LastTradeDate and LastTradeTime fields in the stock quotes 77 | ;;; fetched by get-quotes or just use parse-last-trade-date-time. 78 | ;;; WARNING: use it only for data retrieved from the quotes.csv 79 | ;;; service above! 80 | 81 | (defvar- nyc-date-time-zone 82 | (DateTimeZone/forTimeZone (TimeZone/getTimeZone "America/New_York"))) 83 | 84 | (defn get-correct-date-time [^LocalDate date ^LocalTime time] 85 | (if (and date time) 86 | (let [wrong-date-time (.toDateTime date time nyc-date-time-zone)] 87 | (.withFields (.toDateTime wrong-date-time DateTimeZone/UTC) date)))) 88 | 89 | ;;; 90 | ;;; Get current quotes 91 | ;;; 92 | 93 | (defvar- quote-parse-map 94 | {:symbol identity 95 | :Name identity 96 | :PERatio yql/parse-double 97 | :EarningsShare yql/parse-double 98 | :EPSEstimateNextYear yql/parse-double 99 | :PercebtChangeFromYearHigh yql/parse-double ; typo is actually from data source 100 | :ChangeFromTwoHundreddayMovingAverage yql/parse-double 101 | :TwoHundreddayMovingAverage yql/parse-double 102 | :ChangeinPercent yql/parse-percent 103 | :Bid yql/parse-double 104 | :DaysLow yql/parse-double 105 | :DividendShare yql/parse-double 106 | :ChangeFromYearLow yql/parse-double 107 | :MarketCapitalization yql/parse-double 108 | :YearLow yql/parse-double 109 | :PriceSales yql/parse-double 110 | :ShortRatio yql/parse-double 111 | :PercentChange yql/parse-percent 112 | :EPSEstimateCurrentYear yql/parse-double 113 | :FiftydayMovingAverage yql/parse-double 114 | :ChangeRealtime yql/parse-double 115 | :PercentChangeFromFiftydayMovingAverage yql/parse-percent 116 | :LastTradePriceOnly yql/parse-double 117 | :ExDividendDate yql/parse-date 118 | :DividendPayDate yql/parse-date 119 | :Ask yql/parse-double 120 | :AskRealtime yql/parse-double 121 | :OneyrTargetPrice yql/parse-double 122 | :DividendYield yql/parse-percent 123 | :PercentChangeFromYearLow yql/parse-percent 124 | :PriceBook yql/parse-double 125 | :ChangeFromFiftydayMovingAverage yql/parse-double 126 | :AverageDailyVolume yql/parse-long 127 | :Open yql/parse-double 128 | :PercentChangeFromTwoHundreddayMovingAverage yql/parse-percent 129 | :BidRealTime yql/parse-double 130 | :BookValue yql/parse-double 131 | :LastTradeTime yql/parse-time 132 | :LastTradeDate yql/parse-date 133 | :Change yql/parse-double 134 | :Volume yql/parse-long 135 | :PEGRatio yql/parse-double 136 | :StockExchange identity 137 | :ChangeFromYearHigh yql/parse-double 138 | :DaysHigh yql/parse-double 139 | :YearHigh yql/parse-double 140 | :EBITDA yql/parse-double 141 | :Symbol identity 142 | :PreviousClose yql/parse-double 143 | :PERatioRealtime yql/parse-double 144 | :PriceEPSEstimateNextYear yql/parse-double 145 | :EPSEstimateNextQuarter yql/parse-double 146 | :PriceEPSEstimateCurrentYear yql/parse-double} 147 | "A map from the keys of a raw stock quote to the parsers used to parse 148 | the corresponding value strings of the raw stock quote into useful 149 | typed values (ints, doubles, dates, etc.).") 150 | 151 | (defvar raw-quote-keys (keys quote-parse-map) 152 | "A list of all the keys available in a raw stock quote. A custom stock 153 | quote can be created by supplying `get-quote` with a parser capable of 154 | extracting the value corresponding to a chosen subset of these keys 155 | from a raw stock quote and packaging them into the desired result 156 | structure.") 157 | 158 | (defn parse-quote-item 159 | "Looks up the value of a supplied key into a raw stock quote and 160 | converts it from a string into a more useful type (int, double, date, 161 | time, or keyword depending on the key). It returns `nil` if it cannot 162 | find a valid value." 163 | [raw-quote k] 164 | (if-let [value-parser (get quote-parse-map k)] 165 | (value-parser (get raw-quote k)))) 166 | 167 | (defn parse-last-trade-date-time 168 | "Looks up the last trade date and time in the supplied raw stock quote 169 | and combines them into an `org.joda.time.DateTime` object in UTC. It 170 | works around an inconsistency in the Yahoo! Finance data where the 171 | last trade date is represented in UTC while the last trade time seems 172 | to be using the North American East Coast time zone. It returns `nil` 173 | if a valid `DateTime` object cannot be created." 174 | [raw-quote] 175 | (let [date (parse-quote-item raw-quote :LastTradeDate) 176 | time (parse-quote-item raw-quote :LastTradeTime)] 177 | (get-correct-date-time date time))) 178 | 179 | (defn build-quote-parser 180 | "Returns a parser that can be passed to `get-quote` and `get-quotes` to 181 | get custom stock quotes. The generated parser will return a stock quote 182 | in the form of a map with a set of desired keys. The builder expects a 183 | map from the set of desired keys to the corresponding keys in the raw 184 | stock quote (see `raw-quote-keys` for a list of available keys)." 185 | [key-map] 186 | (fn [raw-quote] 187 | (apply hash-map 188 | (mapcat (fn [[k v]] [k (parse-quote-item raw-quote v)]) key-map)))) 189 | 190 | (defvar- default-key-map 191 | {:symbol :symbol 192 | :name :Name 193 | :last :LastTradePriceOnly 194 | :open :Open 195 | :previous-close :PreviousClose 196 | :high :DaysHigh 197 | :low :DaysLow 198 | :volume :Volume}) 199 | 200 | (defvar- default-quote-parser* (build-quote-parser default-key-map)) 201 | 202 | (defn default-quote-parser 203 | "The quote parser used by `get-quote` and `get-quotes` when a custom quote 204 | parser is not provided. It returns a stock quote in the form of a map 205 | with :symbol, :name, :last-date-time, :last, :open, :previous-close, 206 | :high, :low, and :volume keys." 207 | [raw-quote] 208 | (let [stock-quote (default-quote-parser* raw-quote)] 209 | (assoc stock-quote 210 | :last-date-time (parse-last-trade-date-time raw-quote)))) 211 | 212 | (defn- wrap-error-check 213 | "Augments a stock quote parser to check the raw quote for errors 214 | (typically arising when an invalid stock symbol is required of 215 | `get-quote`). The resulting parser will return `nil` if an error is 216 | found; otherwise, it will invoke the original parser." 217 | [parser] 218 | (fn [r] 219 | (if-not (:ErrorIndicationreturnedforsymbolchangedinvalid r) 220 | (parser r)))) 221 | 222 | (defn get-quotes 223 | "Returns a sequence of stock quotes corresponding to the given stock 224 | symbols. If the first parameter is a function, it uses it as a 225 | parser to turn a raw stock quote (provided by Yahoo! Finance) to a 226 | custom stock quote structure; otherwise, it uses `default-quote-parser`." 227 | [parser & stock-symbols] 228 | (let [[parser stock-symbols] (if (fn? parser) 229 | [parser stock-symbols] 230 | [default-quote-parser (cons parser 231 | stock-symbols)])] 232 | (if stock-symbols 233 | (let [query (str "select * from yahoo.finance.quotes where symbol in " 234 | (yql/yql-string-list (map bare-stock-symbol stock-symbols))) 235 | result (yql/submit-yql-query query)] 236 | (yql/map-parser (wrap-error-check parser) (:quote result)))))) 237 | 238 | (defn get-quote 239 | "Returns the stock quote corresponding to the given stock symbol. 240 | If the first parameter is a function, it uses it as a parser to turn 241 | a raw stock quote (provided by Yahoo! Finance) to a custom stock quote 242 | structure; otherwise, it uses `default-quote-parser`." 243 | ([parser stock-symbol] (first (get-quotes parser stock-symbol))) 244 | ([stock-symbol] (first (get-quotes stock-symbol)))) 245 | 246 | ;;; 247 | ;;; Get historical quotes 248 | ;;; 249 | 250 | ;;; FIXME: consider using type annotations for the double and int/long fields! 251 | (defrecord HistoricalQuote [^LocalDate date open high low close volume]) 252 | 253 | (defn- parse-historical-quote [r] 254 | (let [date (yql/parse-date (:date r)) 255 | open (yql/parse-double (:Open r)) 256 | high (yql/parse-double (:High r)) 257 | low (yql/parse-double (:Low r)) 258 | close (yql/parse-double (:Close r)) 259 | volume (yql/parse-long (:Volume r))] 260 | (HistoricalQuote. date open high low close volume))) 261 | 262 | (defn get-historical-quotes 263 | "Returns a sequence of historical stock quotes corresponding to the 264 | supplied stock symbol, one quote per day between the supplied start 265 | and end dates (as `org.joda.time.LocalDate` objects). Quotes 266 | corresponding to dates falling on weekends and holidays are not 267 | included in the resulting sequence. If quotes for the given symbol 268 | or period cannot be found, it returns `nil`. 269 | 270 | **NOTE:** The Yahoo! Finance service limits the requested period to about 271 | 18 months. If the supplied date range describes a period longer than that 272 | limit, the query returns `nil`." 273 | [^String stock-symbol ^LocalDate start-date ^LocalDate end-date] 274 | (let [query (str "select * from yahoo.finance.historicaldata where symbol = " 275 | (yql/yql-string (bare-stock-symbol stock-symbol)) 276 | " and startDate = " (yql/yql-string start-date) 277 | " and endDate = " (yql/yql-string end-date)) 278 | result (yql/submit-yql-query query)] 279 | (if result 280 | (yql/map-parser parse-historical-quote (:quote result))))) 281 | 282 | ;;; 283 | ;;; Get stocks 284 | ;;; 285 | 286 | (defn- parse-stock [r] 287 | ;; If the requested stock symbol could not be found, the 288 | ;; :CompanyName key will be missing (in this case, the parser returns nil). 289 | (if (:CompanyName r) 290 | (let [stock-symbol (:symbol r) 291 | company-name (:CompanyName r) 292 | start-date (yql/parse-date (:start r)) 293 | end-date (yql/parse-date (:end r)) 294 | sector (:Sector r) 295 | industry (:Industry r) 296 | full-time-employees (yql/parse-int (:FullTimeEmployees r))] 297 | {:symbol stock-symbol 298 | :name company-name 299 | :start-date start-date 300 | :end-date end-date 301 | :sector sector 302 | :industry industry 303 | :full-time-employees full-time-employees}))) 304 | 305 | (defn get-stocks 306 | "Returns a sequence of stock descriptions for the supplied stock symbols. 307 | A stock description is a map with :symbol, :name, :start-date, :end-date, 308 | :sector, :industry, and :full-time-employees keys. 309 | If data for a symbol cannot be found that description will be `nil`." 310 | [& stock-symbols] 311 | (if stock-symbols 312 | (let [query (str "select * from yahoo.finance.stocks where symbol in " 313 | (yql/yql-string-list (map bare-stock-symbol stock-symbols))) 314 | result (yql/submit-yql-query query)] 315 | (yql/map-parser parse-stock (:stock result))))) 316 | 317 | (defn get-stock 318 | "Returns the stock description for the supplied stock symbols. 319 | This description is a map with :symbol, :name, :start-date, :end-date, 320 | :sector, :industry, and :full-time-employees keys. 321 | If data for a symbol cannot be found it returns `nil`." 322 | [stock-symbol] 323 | (first (get-stocks stock-symbol))) 324 | 325 | ;;; 326 | ;;; Industries 327 | ;;; 328 | 329 | (defn- parse-industry-sector [{:keys [name industry]}] 330 | {:name name, :industries (if (vector? industry) industry [industry])}) 331 | 332 | (defn get-industry-sectors 333 | "Returns a sequence of industry sectors. Each sector is a map with 334 | :name and :industries keys. The value of the latter is a vector of 335 | maps corresponding to the industries for that sector. Each industry 336 | is a map with :id and :name fields. The id value can be passed to 337 | `get-industry` to get a list of the companies for a given industry." 338 | [] 339 | (let [query "select * from yahoo.finance.sectors" 340 | result (yql/submit-yql-query query)] 341 | (yql/map-parser parse-industry-sector (:sector result)))) 342 | 343 | (defn- parse-industry [{:keys [id name company]}] 344 | (if id 345 | {:id id, :name name, :companies (if (vector? company) company [company])})) 346 | 347 | (defn get-industries 348 | "Returns a sequence of the industries corresponding to the supplied 349 | industry ids. Each industry is returned as a map with :id, :name, 350 | and :companies keys. The value of the latter is a vector of maps 351 | corresponding to the companies in that industry. Each company is 352 | a map with :name and :symbol keys. The symbol value can be passed to 353 | `get-quote` or `get-historical-quotes` to get stock quotes for that company. 354 | If the industry for a given id cannot be found, its place in the result 355 | sequence will be nil." 356 | [& industry-ids] 357 | (if industry-ids 358 | (let [query (str "select * from yahoo.finance.industry where id in " 359 | (yql/yql-string-list industry-ids)) 360 | result (yql/submit-yql-query query)] 361 | (yql/map-parser parse-industry (:industry result))))) 362 | 363 | (defn get-industry 364 | "Returns the industry corresponding to the supplied industry ids. 365 | The result is a map with :id, :name, and :companies keys. The value 366 | of the latter is a vector of maps corresponding to the companies in 367 | that industry. Each company is a map with :name and :symbol keys. 368 | The symbol value can be passed to `get-quote` or `get-historical-quotes` 369 | to get stock quotes for that company. 370 | If the industry for the supplied id cannot be found, it returns `nil`." 371 | [industry-id] 372 | (first (get-industries industry-id))) 373 | 374 | ;;; 375 | ;;; Get current exchange rate 376 | ;;; 377 | 378 | (defn- parse-exchange-rate [r] 379 | ;; If the requested currency pair could not be found, the 380 | ;; value of the :Name key will be suffixed with "=X" 381 | ;; (in this case, the parser returns nil). 382 | (if-not (re-matches #".*=X" (:Name r)) 383 | (let [id (:id r) 384 | base-currency (keyword (lower-case (subs id 0 3))) 385 | quote-currency (keyword (lower-case (subs id 3 6))) 386 | rate (yql/parse-double (:Rate r)) 387 | ask (yql/parse-double (:Ask r)) 388 | bid (yql/parse-double (:Bid r)) 389 | date (yql/parse-date (:Date r)) 390 | time (yql/parse-time (:Time r)) 391 | date-time (get-correct-date-time date time)] 392 | {:base base-currency 393 | :quote quote-currency 394 | :rate rate 395 | :ask ask 396 | :bid bid 397 | :date-time date-time}))) 398 | 399 | (defn get-exchange-rates 400 | "Returns a sequence of currency exchange rate for all the supplied 401 | currency pairs. Each currency pair is given as a vector of two items: 402 | base and quote currencies, specified as keywords or strings using the 403 | ISO 4217 3-letter designators (e.g. `[:usd :eur]`). 404 | The items in the returned sequence are maps with :base, :quote, :rate, 405 | :ask, :bid, and :date-time keys. 406 | If the exchange rate for a given currency pair cannot be found, its 407 | place in the result sequence will be `nil`." 408 | [& currency-pairs] 409 | (if currency-pairs 410 | (let [pairs (map (fn [[base-currency quote-currency]] 411 | (str (name base-currency) 412 | (name quote-currency))) currency-pairs) 413 | query (str "select * from yahoo.finance.xchange where pair in " 414 | (yql/yql-string-list pairs)) 415 | result (yql/submit-yql-query query)] 416 | (yql/map-parser parse-exchange-rate (:rate result))))) 417 | 418 | (defn get-exchange-rate 419 | "Returns the currency exchange rate for the supplied currency pairs. 420 | The currency pair is given as two separate parameters: base and quote 421 | currencies, specified as keywords or strings using the ISO 4217 422 | 3-letter designators (e.g. `(get-exchange-rate :usd :eur)`). 423 | The result is a map with :base, :quote, :rate, :ask, :bid, and 424 | :date-time keys. 425 | If the exchange rate for the given currency pair cannot be found, it 426 | returns `nil`." 427 | [base-currency quote-currency] 428 | (first (get-exchange-rates [base-currency quote-currency]))) 429 | 430 | ;;; 431 | ;;; Get stock symbol suggestion 432 | ;;; 433 | 434 | (defn- strip-wrapper [^String s] 435 | (let [[s json-string] (re-matches #"YAHOO\.Finance.SymbolSuggest\.ssCallback\((.*)\)" s)] 436 | json-string)) 437 | 438 | (defn get-symbol-suggestion 439 | "Returns zero, one or more suggestions for the stock symbols corresponding 440 | to the supplied company name. Company name prefixes will return results 441 | as well. Note that large companies may be traded on several exchanges and 442 | may thus result in a long list of suggestions." 443 | [^String company-name] 444 | (let [params {:query company-name 445 | :callback "YAHOO.Finance.SymbolSuggest.ssCallback"} 446 | response (client/get "http://autoc.finance.yahoo.com/autoc" 447 | {:query-params params}) 448 | status (:status response)] 449 | (if (not= status 200) 450 | (throw (Exception. (str status)))) 451 | (let [result (-> response :body strip-wrapper read-json :ResultSet :Result)] 452 | (if-not (empty? result) result)))) 453 | 454 | --------------------------------------------------------------------------------