Specter Portfolios

{:deps {org.clojure/clojure {:mvn/version "1.10.3"}
        ;; complient is used for autocompletion
        ;; add your libs here (and restart the runtime to pick up changes)
        compliment/compliment {:mvn/version "0.3.9"}
        com.rpl/specter {:mvn/version "1.1.4"}}}
Extensible Data Notation
(use 'com.rpl.specter)
(require '[clojure.pprint :refer [pprint]])
5.3s

(def port-list1
  "a list of potrfolios"
  [{:id :port1
    :type :portfolio
    :positions [{:id :p1 :units 100 :asset {:id :a1
                                            :type :common-stock
                                            :symbol "IBM"}}
                {:id :p2 :units 200 :asset {:id :a2
                                            :type :common-stock
                                            :symbol "F"}}]}
   {:id :port2
    :type :portfolio
    :positions [{:id :p3 :units 110 :asset {:id :a1
                                            :type :common-stock
                                            :symbol "IBM"}}
                {:id :p4 :units 220 :asset {:id :a2
                                            :type :common-stock
                                            :symbol "F"}}]}])
(def port-list2
  "a list of portfolios with one (bucted) pairs trade"
  [{:id (gensym "port_")
    :type :portfolio
    :positions [{:id (gensym "position_")
                 :type :long-position
                 :units 100
                 :asset {:id (gensym "asset_")
                         :type :common-stock
                         :symbol "IBM"}}
                {:id (gensym "position_") 
                 :type :long-position
                 :units 200
                 :asset {:id (gensym "asset_")
                         :type :common-stock
                         :symbol "F"}}]}
   {:id (gensym "port_")
    :type :portfolio
    :positions [{:id (gensym "position_")
                 :type :long-position
                 :units 110
                 :asset {:id (gensym "asset_")
                         :type :common-stock
                         :symbol "IBM"}}
                {:id (gensym "pairs_")
                 :type :pairs-trade
                 :positions [{:id (gensym "position_")
                              :type :long-position
                              :units 220
                              :asset {:id (gensym "asset_")
                                      :type :common-stock
                                      :symbol "GM"}}
                             {:id (gensym "position_")
                              :type :short-position
                              :units 600
                              :asset {:id (gensym "asset_")
                                      :type :common-stock
                                      :symbol "F"}}]}]}])
(def port-list3
  [{:id (gensym "port_")
    :type :portfolio
    :positions [{:id (gensym "position_")
                 :type :long-position
                 :units 100
                 :asset {:id (gensym "asset_")
                         :type :common-stock
                         :symbol "IBM"}}
                {:id (gensym "position_") 
                 :type :long-position
                 :units 200
                 :asset {:id (gensym "asset_")
                         :type :common-stock
                         :symbol "F"}}]}
   {:id (gensym "port_")
    :type :portfolio
    :positions [{:id (gensym "position_")
                 :type :long-position
                 :units 110
                 :asset {:id (gensym "asset_")
                         :type :common-stock
                         :symbol "IBM"}}
                {:id (gensym "pairs_")
                 :type :pairs-trade
                 :positions [{:id (gensym "position_")
                              :type :long-position
                              :units 220
                              :asset {:id (gensym "asset_")
                                      :type :common-stock
                                      :symbol "GM"}}
                             {:id (gensym "position_")
                              :type :short-position
                              :units 600
                              :asset {:id (gensym "asset_")
                                      :type :common-stock
                                      :symbol "F"}}]}
                {:id (gensym "hedged_")
                 :type :hedged-position
                 :positions [{:id (gensym "position_")
                              :type :long-position
                              :units 500
                              :asset {:id (gensym "asset_")
                                      :type :common-stock
                                      :symbol "AAPL"}}
                             {:id (gensym "position_")
                              :type :hedge
                              :units 500
                              :asset {:id (gensym "asset_")
                                      :type :equity-option
                                      :side :put
                                      :strike 150
                                      :expiry "2023-05-05"
                                      :symbol "AAPL_P150_230550"}}]}]}])
0.1s

Recursive Assets

(select [ALL :positions ALL :asset] port-list1)
0.0s

Note that the 4th asset is nil since the 4th position is a pairs trade (i.e. a bucket or subportfolio)

(select [ALL :positions ALL :asset] port-list2)
0.0s

This will aggressively recurse looking for assets. Here an asset is anything that is in a map that is under the tag :asset.

(def RecursiveAssets
  (identity ;comp-paths
    (recursive-path [] RECURSE
      (cond-path
       sequential? [ALL RECURSE]
       (pred :asset) [:asset STAY]
       map? [MAP-VALS RECURSE]))))
0.1s
;; 5 assets, 3 top level and 2 from a pairs trade
(select RecursiveAssets port-list2)
0.0s
;; 7 assets 3 top level 2 from a pairs trade and 2 from a hedged position
(select RecursiveAssets port-list3)
0.0s
(def prices-1 {"GM" 35.2 "F" 12.60 "IBM" 131.10 "AAPL" 166 "AAPL_P150_230550" 1.65})
;; we can use specter to get another set of prices 50% higher
(def prices-2 (transform MAP-VALS #(* 1.5 %) prices-1))
(defn price-asset
  "price an asset gives prices and an asset
  returns a partial if only prices are provided"
  ;; do a little of out own curry/partial action
  ([prices] (fn price-asset* [asset]
              ;; maybe varargs / options to attch as-of to function meta
              (price-asset prices asset)))
  ([prices asset]
   (if-let [price (-> asset :symbol prices)]
     (assoc asset :price price)
     ;; maybe log an unpriced asset?
     (if (= asset :show-all-prices)
       prices
       asset))))
;price one asset
(price-asset prices-1 {:symbol "F"})
; price all assets
(->> port-list2
  (transform RecursiveAssets (price-asset prices-2))
  (select RecursiveAssets))
0.1s

Recursive Positions

Since a portfolio is a list of positions and/or buckets of positions there are different ways to recurse into them:

  • just at the first level i.e. stop recursion when you find positions

  • full recursion reporting both buckets and the sub-positions .(e.g. position-bucket position-bucket-position1 position-bucket-position2) see below during aggregation this would double count

  • recursion to all "leaf nodes"

(def  RecursivePositions-first
  (comp-paths
    (recursive-path [] RECURSE
      (cond-path
       sequential? [ALL RECURSE]
       (pred :positions) [:positions ALL]
       map? [MAP-VALS RECURSE]))))
(def  RecursivePositions-all
  (comp-paths
    (recursive-path [] RECURSE
      (cond-path
       sequential? [ALL RECURSE]
       (pred :positions) [:positions ALL (stay-then-continue [:positions ALL])]
       map? [MAP-VALS RECURSE]))))
(def  RecursivePositions-leaf
  (comp-paths
    (recursive-path [] RECURSE
      (cond-path
       sequential? [ALL RECURSE]
       (pred :positions) [:positions ALL (stay-then-continue [:positions ALL])]
       map? [MAP-VALS RECURSE]))
    (selected? (must :asset))))
0.2s
;; 3 position + 2 buckets = 5
(->> port-list3
  (select RecursivePositions-first))
0.1s
;; 3 positions + 2 buckets + 4 bucket components = 9
(->> port-list3
  (select RecursivePositions-all))
0.1s
;; 3 positions + 4 bucket components
(->> port-list3
  (select RecursivePositions-leaf))
0.1s
(defn price-portfolio
  "aggregate all price*units for leaf nodes"
  [port]
  (->> port
     (select RecursivePositions-leaf)
     (transduce (map (fn [p] (* (:units p) (get-in p [:asset :price])))) + )))
0.0s
;; por portfolios as they were provided did not have the assets price so we do that first
(->> port-list3
  (transform RecursiveAssets (price-asset prices-1))
  price-portfolio)
0.0s
;; price for each portfolio (top-level)
(->> port-list3
  (transform RecursiveAssets (price-asset prices-1))
  (map (juxt :id price-portfolio))
  (into {}))
0.0s
(defrecord Portfolio [id positios])
(defrecord Position [id asset units])
(defrecord PositionBucket [id positions])
(defrecord Asset [id type])
(defprotocolpath AssetPath [])
(extend-protocolpath AssetPath
  Position :asset
  Portfolio [:positions ALL AssetPath]
  PositionBucket [:positions ALL AssetPath])
0.4s
;; make port-list2 have Position and Asset record types
(try
(->> port-list3
  (transform [ALL]
        map->Portfolio)
  (transform [ALL :positions ALL (selected? :type #{:long-position :short-position})]
        map->Position)
  (transform [ALL :positions ALL (selected? :type #{:pairs-trade :hedged-position})]
        map->PositionBucket)
  (transform [RecursiveAssets]
    map->Asset))
  (catch Exception e (str "Why???? "e))
)
0.1s
(defn map->PositionBucket-deep
  [{:keys [id positions] :as input-map}]
  (->PositionBucket id (mapv map->Position positions)))
(defn map->Position-deep
  [{:keys [id asset units] :as input-map}]
  (->Position id (map->Asset asset) units))
(defn map->PositionBucket-deeper
  [{:keys [id positions] :as input-map}]
  (->PositionBucket id (mapv map->Position-deep positions)))
0.1s
(->> port-list3
  (transform [ALL]
        map->Portfolio)
  (transform [ALL :positions ALL (selected? :type #{:long-position :short-position})]
        map->Position-deep)
  (transform [ALL :positions ALL (selected? :type #{:pairs-trade :hedged-position})]
        map->PositionBucket-deeper)
  (def PortList3))
0.1s
(try
  (->> port-list3
    (select [ALL AssetPath]))
  (catch Exception e (str "this should fail sice port-list3 is maps not records")))
0.1s
;; these queries are equal now.  AssetPath requires records and more setup.
;; it also is more controlled , stoping navigation where is is not needed
(=
  (select [RecursiveAssets] PortList3)
  (select [ALL AssetPath] PortList3))
0.0s

Error on re-construct

Why does the RecursiveAssets fail on transform?

(->> PortList3
  ;; this fails
  #_(transform [RecursiveAssets] (price-asset prices-2))
  ;; this works
  (transform [ALL AssetPath] (price-asset prices-2)))
0.4s
0.0s
Runtimes (1)