Building a strategy
Our objective is to write code that will allow us to do something like
samplePeriod
|> filterInvestmentUniverse
|> constructSignals
|> assignPortfolioWeights
|> getPortfolioReturns
Now let's dig in. This is a relatively basic version of a strategy to build intuition with what we're doing. It is simpler than working with real data. But we will next move to real data.
#r "nuget: FSharp.Stats, 0.5.0"
open FSharp.Stats
Workflow for using quantitative signals
How do we decide on the position weights for the different securities? An example workflow:
- Define your investment universe. Your investment universe is the set of securities that you are considering for investment. An investment universe is typically a subset of securities grouped by some common characteristics: small-capitalization stocks, utility stocks, corporate bonds, firms listed on Euronext, etc.
- Get your signal for each security. The signal is some information that you believe can be used to predict returns or risk. It could be a stock tip from a friend, a stock analyst recommendation, something you read on reddit, revenue growth, ...
- Define a mapping from signals to portfolio weights.
Let's go through this step by step.
1. Investment universe: modelling a security
We're going to be forming portfolios consisting of weights on different securities. We need a way to identify those securities. Securities can have different identifiers. Stock exchanges use ticker symbols, but CUSIPs are commonly used by other entities (e.g., ratings agencies, regulators, etc.). Some data providers might also use their own custom identifier. For example, Bloomberg has their own "Bloomberg ticker" and CRSP uses PERMNOs. Thus, if we're trying to identify a security we can use tickers OR CUSIPs OR Bloomberg Tickers OR PERMNOs. How do we model this?
A good type for modelling "OR" relationships is a Discriminated Union. The pipe (|
) symbol indicates an "or" relationship.
type SecurityId =
| Ticker of string
| Cusip of string
| Bloomberg of string
| Permno of int
// Defining examples of these ids.
let tickerExample = Ticker "KO"
let permnoExample = Permno 1001
// Deconstructing using pattern matching.
// Think "match what's on the left side of = to what's on the right side"
let (Ticker deconTickerExample) = tickerExample
printfn "%s" deconTickerExample
let (Permno deconPermnoExample) = permnoExample
printfn "%i" deconPermnoExample
// Now we can define our investment universe.
let investmentUniverse =
[for tick in [ "AAPL"; "KO"; "GOOG";"DIS";"GME"] do
Ticker tick ]
2. Signals
let signals =
[ Ticker "AAPL", 2.0
Ticker "KO", -1.4
Ticker "GOOG", 0.4
Ticker "DIS", 1.1 ]
|> Map
signals[Ticker "AAPL"]
Map.find (Ticker "AAPL") signals
|
Map.tryFind (Ticker "AAPL") signals
|
Map.tryFind (Ticker "GME") signals
|
Now a function that gets the signal given a security id.
let getSignal id = Map.tryFind id signals
getSignal (Ticker "AAPL")
getSignal (Ticker "GOOG")
We can call this function on all signals in our investment universe.
// using a loop.
[ for security in investmentUniverse do
security, getSignal security ]
// same thing with List.map
investmentUniverse
|> List.map(fun security -> security, getSignal security)
Let's create a type to hold a security identifier and its signal.
type SecuritySignal =
{ SecurityId : SecurityId
Signal : float }
Now a function that gets the signal and puts it in the
SecuritySignal
record type. It's possible that the security signal does not exist, so we'll use an option type to handle the fact that we might
find the signal and we might not.
let getSecuritySignal security =
match Map.tryFind security signals with
| None -> None
| Some signal ->
let result = { SecurityId = security; Signal = signal }
Some result
Here we can see that we retured something when there was a signal and nothing if there was none.
getSecuritySignal (Ticker "GME")
|
getSecuritySignal (Ticker "GOOG")
|
for security in investmentUniverse do
let securitySignal = getSecuritySignal security
printfn $"{securitySignal}"
|
This is equivalent but using a pipeline.
investmentUniverse
|> List.map getSecuritySignal
|> List.iter (printfn "%A")
|
If we do choose instead of map, then we will end up with only the results when there was something.
investmentUniverse
|> List.choose getSecuritySignal
|> List.iter (printfn "%A")
|
let securitySignals =
investmentUniverse
|> List.choose getSecuritySignal
securitySignals
|> List.iter (printfn "%A")
|
3. Defining a mapping between signals and portfolio weights.
Now let's look at turning signals into weights.
Often, you only want stocks with signals above or below a given threshold in a portfolio. For instance, if you have a "value" portfolio you might only want stocks with low price to earnings (P/E) ratios in your portfolio. Or maybe you want to go long value stocks and short growth stocks.
A typical procedure is to assign securities to portfolios based on signals, and then weight securities within those sub portfolios.
First, let's represent portfolios Ids. A first step is to define portfolio IDs. A simple ID is a string name, but often we will do things like create 10 size portfolios infexed from 1 to 10, like ("Size", 1), ("Size", 2), ... We can model this as a discriminated union.
Here, Indexed
is a tuple where the first element is a string and the second is an integer. I could just say Indexed of string * int
, but I am going to name them to give them meaning. Though the names are optional when constructing and deconstructing
type PortfolioId =
| Named of string
| Indexed of {| Name: string; Index: int |}// name:string * index:int
// Example portfolio IDs
let portfolioId1 = Named "Market"
let portfolioId2 = Indexed {| Name = "Size"; Index = 1 |}
let portfolioId3 = Indexed {| Name = "Size" ; Index = 2 |}
let getPortfolioIdString port =
match port with
| Named name -> name
| Indexed p -> $"{p.Name}: {p.Index}"
getPortfolioIdString portfolioId1
|
getPortfolioIdString portfolioId3
|
Let's assign securities to portolios based on whether their signal is above or below the median.
// Model for an assigned portfolio
type AssignedPortfolio =
{ PortfolioId : PortfolioId
Signals : list<SecuritySignal> }
let medianSignal =
securitySignals
|> List.map(fun x -> x.Signal)
|> Seq.median
The median signal is
|
let aboveMedian =
securitySignals
|> List.filter(fun x -> x.Signal >= medianSignal)
the above-median securities:
|
let belowMedian =
securitySignals
|> List.filter(fun x -> x.Signal < medianSignal)
the below-median securities:
|
let assigned =
[ { PortfolioId = Named("Above Median")
Signals = aboveMedian }
{ PortfolioId = Named("Below Median")
Signals = belowMedian} ]
assigned to portfolios:
|
Or create a reusable function to do the same thing
let assignAboveBelowMedian securitySignals =
let medianSignal =
securitySignals
|> List.map(fun x -> x.Signal)
|> Seq.median
let aboveMedian =
securitySignals
|> List.filter(fun x -> x.Signal >= medianSignal)
let belowMedian =
securitySignals
|> List.filter(fun x -> x.Signal < medianSignal)
[ { PortfolioId = Named("Above Median")
Signals = aboveMedian }
{ PortfolioId = Named("Below Median")
Signals = belowMedian} ]
Modelling a position
Now we have assigned securities to portfolios based on the trading signal. Now we can form weights. We can think of a portfolio as consisting of positions where positions are symbols and weights.
type Position =
{ SecurityId : SecurityId
Weight : float }
// Defining example positions
let koPosition = { SecurityId = Ticker "KO"; Weight = 0.25 }
let permnoPosition = { SecurityId = Permno 1001; Weight = 0.75 }
Modelling a portfolio
And once we have multiple positions, then we can group them into a portfolio.
And a portfolio can consist of a Portfolio Id and an List of positions
type Portfolio =
{ PortfolioId: PortfolioId
Positions : list<Position> }
An example constructing a portfolio
let portfolioExample1 =
{ PortfolioId = Named "Example 1"
Positions = [ koPosition; permnoPosition ] }
Defining portfolio position weights
Once you have a portfolio of securities that have met some signal criteria, it is common to weight those securities using either of two simple weighting schemes: equal weight or value weight.
Equal weight means that every security has the same weight.
Value-weight means that you weight securities proportional to their market value. This means that your portfolio put more weight on more valuable securities. Or it "tilts" toward more valuable securities. This weighting scheme is utilized when you want to make sure that you are not putting too much weight on small illiquid securities that are hard to purchase.
Equal-weight is easy:
let weightTestPort =
assigned |> List.find (fun x -> x.PortfolioId = Named("Above Median"))
let nSecurities = weightTestPort.Signals.Length
let ewTestWeights =
[ for signal in weightTestPort.Signals do
{ SecurityId = signal.SecurityId
Weight = 1.0 / (float nSecurities) } ]
let giveEqualWeights x =
let n = x.Signals.Length
let pos =
[ for signal in x.Signals do
{ Position.SecurityId = signal.SecurityId
Weight = 1.0 / (float n) } ]
{ PortfolioId = x.PortfolioId
Positions = pos }
For one portfolio:
giveEqualWeights weightTestPort
|
For all portfolios:
[ for portfolio in assigned do giveEqualWeights portfolio ]
|
or equivalently:
assigned |> List.map giveEqualWeights
|
For value weights, we need the securities' market values split into above/below median and for portfolios with those.
let marketCapitalizations =
[ Ticker "AAPL", 10.0
Ticker "KO", 4.0
Ticker "GOOG", 7.0
Ticker "DIS", 5.0 ]
|> Map
let mktCaps =
[ for signal in weightTestPort.Signals do
let mktcap = Map.find signal.SecurityId marketCapitalizations
signal.SecurityId, mktcap ]
|
let vwTestWeights =
let totMktCap = mktCaps |> List.sumBy snd
[ for (id, mktCap) in mktCaps do
{ SecurityId = id
Weight = mktCap / totMktCap } ]
|
Now a function to do the same as above.
let giveValueWeights x =
let mktCaps =
[ for signal in x.Signals do
let mktcap = Map.find signal.SecurityId marketCapitalizations
signal.SecurityId, mktcap ]
let totMktCap = mktCaps |> List.sumBy snd
let pos =
[ for (id, mktCap) in mktCaps do
{ SecurityId = id
Weight = mktCap / totMktCap } ]
{ PortfolioId = x.PortfolioId; Positions = pos }
We can map our function to both of our assigned portfolios.
[ for x in assigned do giveValueWeights x ]
|
or equivalently
assigned |> List.map giveValueWeights
|
All together now. This is our workflow.
let strategyWeights =
investmentUniverse
|> List.choose getSecuritySignal
|> assignAboveBelowMedian
|> List.map giveValueWeights
|
How do we calculate returns?
Take these returns:
let returnMap =
[ Ticker "AAPL", -0.4
Ticker "KO", -0.1
Ticker "GOOG", 0.15
Ticker "DIS", 0.1 ]
|> Map
What is the return of the two portfolios?
Hint: the value-weight assignment code is a good reference for figuring out how to look up the stock returns.
namespace FSharp
--------------------
namespace Microsoft.FSharp
val string: value: 'T -> string
--------------------
type string = System.String
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
module Map from FSharp.Stats
<summary> Module to strore specialised computations on maps </summary>
--------------------
module Map from Microsoft.FSharp.Collections
--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> interface IEnumerable interface IStructuralEquatable interface IComparable interface IEnumerable<KeyValuePair<'Key,'Value>> interface ICollection<KeyValuePair<'Key,'Value>> interface IDictionary<'Key,'Value> new: elements: ('Key * 'Value) seq -> Map<'Key,'Value> member Add: key: 'Key * value: 'Value -> Map<'Key,'Value> ...
--------------------
new: elements: ('Key * 'Value) seq -> Map<'Key,'Value>
module List from FSharp.Stats
<summary> Module to compute common statistical measure on list </summary>
--------------------
module List from Microsoft.FSharp.Collections
--------------------
type List = new: unit -> List static member geomspace: start: float * stop: float * num: int * ?IncludeEndpoint: bool -> float list static member linspace: start: float * stop: float * num: int * ?IncludeEndpoint: bool -> float list
--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
--------------------
new: unit -> List
val float: value: 'T -> float (requires member op_Explicit)
--------------------
type float = System.Double
--------------------
type float<'Measure> = float
module Seq from FSharp.Stats
<summary> Module to compute common statistical measure </summary>
--------------------
module Seq from Microsoft.FSharp.Collections
--------------------
type Seq = new: unit -> Seq static member geomspace: start: float * stop: float * num: int * ?IncludeEndpoint: bool -> float seq static member linspace: start: float * stop: float * num: int * ?IncludeEndpoint: bool -> float seq
--------------------
new: unit -> Seq
<summary> Sample Median </summary>