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