Phil Cooper / Apr 17 2023 / Published
Remix of Specter by
Phil Cooper
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