Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 45 additions & 42 deletions src/stockings/exchanges.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down Expand Up @@ -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])
Expand All @@ -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]
Expand All @@ -147,4 +151,3 @@
(first (filter (fn [c] (= exchange-key (:exchange c))) res))
(if (= exchange-key (:exchange res)) res))
res)))))

23 changes: 23 additions & 0 deletions src/stockings/utils.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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))))))))