diff --git a/src/stockings/exchanges.clj b/src/stockings/exchanges.clj index 5739b0f..b53ae75 100644 --- a/src/stockings/exchanges.clj +++ b/src/stockings/exchanges.clj @@ -6,27 +6,34 @@ (:use [clojure.string :only (split lower-case upper-case)] [clojure.contrib.def :only (defvar defvar-)] [clojure-csv.core :only (parse-csv)] - [stockings.core :only (explode-stock-symbol)]) + [stockings.core :only (explode-stock-symbol)] + [stockings.utils :only (interleave-if)]) (:require [clj-http.client :as client])) ;;; ;;; Stock Exchanges ;;; +(defvar- source-url "http://www.nasdaq.com/screening/companies-by-name.aspx") +(defvar- default-record-keys [:stock-symbol :name :n/a :n/a :ipo-year :sector :industry :n/a :n/a]) + (defvar nasdaq - {:name "NASDAQ Stock Market", :symbol "NASDAQ"} + {:name "NASDAQ Stock Market", :symbol "NASDAQ" + :source-url source-url :record-keys default-record-keys} "A map describing the NASDAQ Stock Market (NASDAQ).") (defvar nyse - {:name "New York Stock Exchange", :symbol "NYSE"} + {:name "New York Stock Exchange", :symbol "NYSE" + :source-url source-url :record-keys default-record-keys} "A map describing the New York Stock Exchange (NYSE).") (defvar amex - {:name "NYSE Amex Equities", :symbol "AMEX"} + {:name "NYSE Amex Equities", :symbol "AMEX" + :source-url source-url :record-keys default-record-keys} "A map describing the NYSE Amex Equities (AMEX).") (defvar exchanges - {:amex amex, + {:amex amex :nasdaq nasdaq :nyse nyse} "A map from stock exchange keywords to stock exchange info maps.") @@ -54,8 +61,6 @@ ;;; Companies ;;; -(defvar- source-url "http://www.nasdaq.com/screening/companies-by-name.aspx") - ;; Use a record instead of a map for efficiency since the lists of ;; companies are pretty long. (defrecord Company [exchange symbol name ipo-year sector industry]) @@ -65,64 +70,63 @@ The test is very basic and mainly serves to eliminate blank lines and the ending comma (which causes the CSV parser to return an extra record) from the parsed CSV file." - [r] + [r-keys r] (and (vector? r) - (= 9 (count r)) + (= (count r-keys) (count r)) (< 0 (count (first r))))) -(defn- convert-record +(defn new-company + "Constructor function for Company datastructure. Requirest at least the named + parameters :exchange-key :stock-symbol :name. The remaining parameters are + optional and default to nil if not provided." + [& {:keys [exchange-key stock-symbol name ipo-year sector industry] + :or {ipo-year nil sector nil industry nil}}] + (Company. exchange-key stock-symbol name ipo-year sector industry)) + +(defn convert-record [exchange-key r-keys r] "Transforms a CSV record into a Company record. It assumes the CSV record is a sequence of strings corresponding to the - following fields: symbol, name, last sale, market capitalization, IPO year, - sector, industry, URL for summary quote. + fields specified in r-keys. The resulting Company record retains the exchange, symbol, name, IPO year, sector, and industry for the company." - [exchange-key r] - (let [stock-symbol (upper-case (nth r 0)) - name (nth r 1) - ipo-year (let [field (nth r 4)] - (if (re-find #"^\d{4}$" field) (Integer/parseInt field 10))) - sector (nth r 5) - industry (nth r 6)] - (Company. exchange-key stock-symbol name ipo-year sector industry))) + (apply (partial new-company :exchange-key exchange-key) + (interleave-if (fn [x] (not= x :n/a)) r-keys r))) (defn parse-companies "Parses a string of CSV-encoded companies and returns them as a sequence of Company records. It expects one company record per - line with the following fields: symbol, name, last sale, market - capitalization, IPO year, sector, industry, and URL for summary quote. + line. The first line is expected to contain the column headers and is discarded. The first parameter should be a key representing the exchange on which all the companies described in the input string are traded. The result includes exchange, symbol name, IPO year, sector, and industry for each company." [exchange-key ^String s] - (->> s - parse-csv - rest - (filter valid-record?) - (map (partial convert-record exchange-key)))) - -(defn get-companies - "Requests a list of the companies traded on the stock exchange denoted by - the supplied keyword. If no keyword is provided, it returns a merged list - of the companies traded on the NASDAQ, NYSE, and AMEX exchanges. - The companies are returned as a sequence of Company records. - See `parse-companies` for details." + (let [r-keys (:record-keys (exchange-key exchanges))] + (->> s + parse-csv + (filter (partial valid-record? r-keys)) + rest + (map (partial convert-record exchange-key r-keys))))) + +(defn get-companies-request + "Requests a list of the companies traded on the stock exchange denoted by the supplied keyword. + If no keyword is provided, it returns a merged string of all companies traded on the exchanges + specified in exchanges." ([exchange-key] (let [params {:render "download", :exchange (name exchange-key)} - request (client/get source-url {:query-params params})] - (parse-companies exchange-key (:body request)))) + source-url (:source-url (exchange-key exchanges))] + (:body (client/get source-url {:query-params params})))) ([] - (mapcat (fn [exchange-key] (get-companies exchange-key)) (keys exchanges)))) + (mapcat (fn [exchange-key] (get-companies exchange-key) (keys exchanges))))) (defn- build-companies-map [companies] (letfn [(merge-entry [m c] - (let [k (:symbol c)] - (if-let [v (get m k)] - (assoc m k (if (vector? v) (conj v c) [vector v c])) - (assoc m k c))))] + (let [k (:symbol c)] + (if-let [v (get m k)] + (assoc m k (if (vector? v) (conj v c) [vector v c])) + (assoc m k c))))] (reduce merge-entry {} companies))) (defn- normalize-stock-symbol [^String stock-symbol] @@ -147,4 +151,3 @@ (first (filter (fn [c] (= exchange-key (:exchange c))) res)) (if (= exchange-key (:exchange res)) res)) res))))) - diff --git a/src/stockings/utils.clj b/src/stockings/utils.clj index 161d57e..4f2b508 100644 --- a/src/stockings/utils.clj +++ b/src/stockings/utils.clj @@ -143,3 +143,26 @@ (if s (.toLocalTime (.parseDateTime time-parser s)))) + +(defn interleave-if + "Returns a lazy seq of the first item in each coll, then the second etc. Pred is applied to + the elements of the first collection, noly adding elements of the first and subsequent collections + if it holds." + {:added "1.0" + :static true} + ([pred c1 c2] + (lazy-seq + (let [s1 (seq c1) s2 (seq c2)] + (when (and s1 s2) + (let [first-s1 (first s1)] + (if (pred first-s1) + (cons first-s1 (cons (first s2) + (interleave-if pred (rest s1) (rest s2)))) + (interleave-if pred (rest s1) (rest s2)))))))) + ([pred c1 c2 & colls] + (lazy-seq + (let [ss (map seq (conj colls c2 c1))] + (when (every? identity ss) + (concat (map first ss) (apply (partial interleave-if pred) (map rest ss)))))))) + +