Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
127 lines (100 sloc) 6.41 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
(ns sell-in-may.analysis-3b
(:import (org.joda.time LocalDate))
(:require [incanter.core :as ic]
[incanter.stats :as is]
[incanter.charts :as icharts]
[ :as io]
[ :as csv]
[clojure.pprint :as pp]))
; Load raw CSV data
(def raw-data (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))
(defn string-date->local-date [s]
"Convert a yyyy-MM-dd 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)))
; 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))))
; 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) ; Use this for risk-free when out-of-market
(if out-of-market (* 0.5 returns) (* 1.5 returns)) ; Use this for "smart beta" sell-in-May strategy
; 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)))
(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}))
(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)))
; Look at alpha distribution
(defn get-alpha-beta [passive-time-returns active-time-returns]
"Helper function to return alpha, t-prob and beta for a given pair of passive/active returns"
(let [lm-stats (is/linear-model (vals active-time-returns) (vals passive-time-returns))
alpha (-> lm-stats :coefs first)
beta (-> lm-stats :coefs second)
annualised-alpha (* 12 alpha)
t-prob (-> lm-stats :t-probs first)]
(vec [annualised-alpha t-prob beta])))
; Split the overall time period into 10 year parts, starting at 1955..
(def time-range (range 1955 2016 10))
(def time-periods (map #(vec [%1 %2]) time-range (rest time-range)))
(defn time-returns->sub-time-returns [sc from-year to-year]
"Take a sorted map by millis and returns a submap by given [from-year, to-year]"
(let [from-millis (-> (LocalDate. from-year 1 1) .toDateMidnight .getMillis)
to-millis (-> (LocalDate. to-year 1 1) .toDateMidnight .getMillis)]
(into (sorted-map) (subseq sc >= from-millis < to-millis))))
(def passive-periods (into (sorted-map) (map #(vec [%1 %2])
(map #(str "[" (first %) ", " (second %) ")") time-periods)
(map #(time-returns->sub-time-returns time-returns (first %) (second %)) time-periods))))
(def active-periods (into (sorted-map) (map #(vec [%1 %2])
(map #(str "[" (first %) ", " (second %) ")") time-periods)
(map #(time-returns->sub-time-returns active-time-returns (first %) (second %)) time-periods))))
(def active-vs-passive-periods (into (sorted-map) (map #(vec [(first %1) (get-alpha-beta (second %1) (second %2))]) passive-periods active-periods)))
(pp/pprint active-vs-passive-periods)
(def passive-price-periods (into (sorted-map) (map #(vec [(str "Passive " (first %)) (:m (reduce accumulate-returns {:m (sorted-map) :r 0.0} (second %)))]) passive-periods)))
(def active-price-periods (into (sorted-map) (map #(vec [(str "Active " (first %)) (:m (reduce accumulate-returns {:m (sorted-map) :r 0.0} (second %)))]) active-periods)))
(ic/view (reduce add-to-chart nil (concat passive-price-periods active-price-periods)))
; Get passive and active normalized prices based on passive and active returns
(def all-passive-time-price (:m (reduce accumulate-returns {:m (sorted-map) :r 0.0} time-returns)))
(def all-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" all-active-time-price])) (vec ["Passive TR" all-passive-time-price])))
; Get some simple alpha/beta stats:
(def lm-stats (is/linear-model (vals active-time-returns) (vals time-returns)))
(println (str "lm-stats coefs: " (:coefs lm-stats)))
(println (str "lm-stats t-probs: " (:t-probs lm-stats)))
(def last-active-value (-> all-active-time-price last last))
(def last-passive-value (-> all-passive-time-price last last))
(println (str "Last active value: " last-active-value))
(println (str "Last passive value: " last-passive-value))