Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
112 lines (91 sloc) 5.97 KB
; This is the code used to generate the graphs and data used to look into
; the "Sell in May" stock investment strategy. The write-up of this can be
; found on http://www.agoraopus.com/blog/2015/sell-in-may-and-go-away-part-3
(ns sell-in-may.analysis-3a
(:import (org.joda.time LocalDate))
(:require [incanter.core :as ic]
[incanter.stats :as is]
[incanter.charts :as icharts]
[clojure.java.io :as io]
[clojure.data.csv :as csv]
[clojure.pprint :as pp]))
; Load raw CSV data
(def raw-data (with-open [in-file (io/reader "../../resources/monthly_sp500.csv")]
(doall (csv/read-csv in-file))))
(def raw-data-tr (with-open [in-file (io/reader "../../resources/monthly_sp500_tr.csv")]
(doall (csv/read-csv in-file))))
; Skip header
(def raw-no-header (rest raw-data))
(def raw-no-header-tr (rest raw-data-tr))
(defn string-date->local-date [s]
"Convert a yyyy-MM string pattern to a LocalDate"
(let [parts (.split s "-")
year (Integer/parseInt (aget parts 0))
month (Integer/parseInt (aget parts 1))
day 15]
(LocalDate. year month day)))
; Format raw data into a sorted map keyed by unix millis and a double price value
(def time-price (into (sorted-map) (map #(vec [(-> % first string-date->local-date .toDateMidnight .getMillis) (-> % last read-string)]) raw-no-header)))
(def time-price-tr (into (sorted-map) (map #(vec [(-> % first string-date->local-date .toDateMidnight .getMillis) (-> % last read-string)]) raw-no-header-tr)))
(defn add-to-chart [chart [label data]]
"Helper function to add data series to a time series plot"
(if (nil? chart)
(icharts/time-series-plot (keys data) (vals data) :series-label label :legend true)
(icharts/add-lines chart (keys data) (vals data) :series-label label :legend true)))
(defn add-to-bar-chart [chart [label data]]
"Helper function to add data series to a time series plot"
(if (nil? chart)
(icharts/bar-chart (keys data) (vals data) :series-label label :legend true)
(icharts/add-categories chart (keys data) (vals data) :series-label label :legend true)))
(defn accumulate-returns [{output :m r-sum :r} i]
"A helper function to convert accumulated returns back into a normalized price"
(let [millis (first i)
returns (second i)
new-r-sum (+ r-sum returns)
new-output (assoc output millis (* 100 (Math/exp new-r-sum)))]
{:m new-output :r new-r-sum}))
; Convert price to log-returns, keyed by millis in a sorted map
(def time-returns (into (sorted-map) (map #(vec [(first %2) (Math/log (/ (second %2) (second %1)))]) time-price (rest time-price))))
(def time-returns-tr (into (sorted-map) (map #(vec [(first %2) (Math/log (/ (second %2) (second %1)))]) time-price-tr (rest time-price-tr))))
; Plot renormalized prices
(def normalized-time-price (:m (reduce accumulate-returns {:m (sorted-map) :r 0.0} time-returns)))
(def normalized-time-price-tr (:m (reduce accumulate-returns {:m (sorted-map) :r 0.0} time-returns-tr)))
(ic/view (reduce add-to-chart nil (vec [["S&P 500" normalized-time-price] ["S&P 500 total returns" normalized-time-price-tr]])))
; Group returns my month..
(def returns-by-month (group-by #(-> % first LocalDate. .getMonthOfYear) time-returns))
(def returns-by-month-tr (group-by #(-> % first LocalDate. .getMonthOfYear) time-returns-tr))
; Get average returns per month and view in bar chart
(def avg-returns-by-month (into (sorted-map) (map #(vec [(first %) (is/mean (map second (second %)))]) returns-by-month)))
(def avg-returns-by-month-tr (into (sorted-map) (map #(vec [(first %) (is/mean (map second (second %)))]) returns-by-month-tr)))
(ic/view (reduce add-to-bar-chart nil (vec [["S&P 500" avg-returns-by-month] ["S&P 500 total returns" avg-returns-by-month-tr]])))
; Get median returns per month and view in bar chart
(def med-returns-by-month (into (sorted-map) (map #(vec [(first %) (is/median (map second (second %)))]) returns-by-month)))
(def med-returns-by-month-tr (into (sorted-map) (map #(vec [(first %) (is/median (map second (second %)))]) returns-by-month-tr)))
(ic/view (reduce add-to-bar-chart nil (vec [["S&P 500" med-returns-by-month] ["S&P 500 total returns" med-returns-by-month-tr]])))
; To apply the "Sell in May" strategy with above data we stay out of the market when the month
; is one of May (5), June (6), July (7), August (8), September (9), and October (10).
; Ideally we'd use some historical "risk free" rate for when we're out of the market,
; but we'll keep it simple here and apply a fixed annual geometric rate as defined below.
(def risk-free 0.05)
(defn get-active-return [oom-fn risk-free [millis returns]]
"Get the active return from either market or risk-free depending on month"
(let [local-date (LocalDate. millis)
month (.getMonthOfYear local-date)
out-of-market (oom-fn month)]
(if out-of-market (/ risk-free 12) returns)))
; As the time-returns are indexed by the following month, April returns are shown on month 5, etc..
(defn oom-may-october [month]
"Returns true when we're staying out of market"
(let [m-1 (dec month)]
(get {5 true, 6 true, 7 true, 8 true, 9 true, 10 true} m-1 false)))
; Our active returns, keyed by millis as earlier..
(def active-time-returns (into (sorted-map) (map #(vec [(first %) ((partial get-active-return oom-may-october risk-free) %)]) time-returns-tr)))
; Get passive and active normalized prices based on passive and active returns
(def passive-time-price (:m (reduce accumulate-returns {:m (sorted-map) :r 0.0} time-returns-tr)))
(def active-time-price (:m (reduce accumulate-returns {:m (sorted-map) :r 0.0} active-time-returns)))
; Plot passive and active together..
(ic/view (add-to-chart (add-to-chart nil (vec ["Active TR" active-time-price])) (vec ["Passive TR" passive-time-price])))
; Get some simple alpha/beta stats:
(def lm-stats (is/linear-model (vals active-time-returns) (vals time-returns-tr)))
(println (str "lm-stats coefs: " (:coefs lm-stats)))
(println (str "lm-stats t-probs: " (:t-probs lm-stats)))