Financial Engineering: Static Replication of any Payoff Function


In the area of option strategy trading, it has always been a dream of mine to have a universal tool that is able to replicate any payoff function statically by combining plain vanilla products like calls, puts, and zerobonds.

Many years ago there was such a tool online but it has long gone since and the domain is inactive. So, based on the old project paper from that website I decided to program it in R and make it available for free here!

The project paper the algorithm is based on and which is translated to R can be found here: Financial Engineering Tool: Replication Strategy and Algorithm. I will not explain the algorithm as such and how it works because this is done brilliantly in the paper. Also, I won’t get into any details concerning derivatives and structured products either. You can find tons of material on the web just by googling. So, without further ado let’s get started!

First, we need a way to define the payoff function: for each kink we provide two values, one for the underlying which goes from 0 to infinity and one for the payoff we want to replicate. We will use the names used in the paper for all the needed variables for clarity. Let us start by defining a plain vanilla call:

payoff <- data.frame(pi = c(0, 100, 110, Inf), f_pi = c(0, 0, 10, Inf))
payoff
##    pi f_pi
## 1   0    0
## 2 100    0
## 3 110   10
## 4 Inf  Inf

The last value of the payoff must be either equal to the penultimate value (= payoff staying flat at the given value) or must be (minus) infinity for a linear continuation in the given direction. Next we want to plot this payoff:

plot_payoff <- function(payoff, xtrpol = 1.5) {
  k <- nrow(payoff) - 1
  payoff_prt <- payoff
  payoff_prt$pi[k+1] <- payoff$pi[k] * xtrpol
  # linear extrapolation of last kink
  slope <- diff(c(payoff$f_pi[k-1], payoff$f_pi[k])) / diff(c(payoff$pi[k-1], payoff$pi[k]))
  payoff_prt$f_pi[k+1] <- ifelse(payoff$f_pi[k] == payoff$f_pi[k+1], payoff$f_pi[k+1], payoff$f_pi[k] + payoff$pi[k] * (xtrpol - 1) * slope)
  plot(payoff_prt, ylim = c(-max(abs(payoff_prt$f_pi) * xtrpol), max(abs(payoff_prt$f_pi) * xtrpol)), main = "Payoff diagram", xlab = "S(T)", ylab = "f(S(T))", type = "l")
  abline(h = 0, col = "blue")
  grid()
  lines(payoff_prt, type = "l")
  invisible(payoff_prt)
}

plot_payoff(payoff)

Now comes the actual replication. We need to functions for that: a helper function to calculate some parameters…

calculate_params <- function(payoff) {
  params <- payoff
  k <- nrow(params) - 1
  
  # add additional columns s_f_pi, lambda and s_lambda
  params$s_f_pi <- ifelse(params$f_pi < 0, -1, 1)
  # linear extrapolation of last kink
  slope <- diff(c(params$f_pi[k-1], params$f_pi[k])) / diff(c(params$pi[k-1], params$pi[k]))
  f_pi_k <- ifelse(params$f_pi[k] == params$f_pi[k+1], params$f_pi[k+1], slope)
  params$lambda <- c(diff(params$f_pi) / diff(params$pi), f_pi_k)
  params$s_lambda <- ifelse(params$lambda < 0, -1, 1)
  # consolidate
  params[k, ] <- c(params[k, 1:3], params[(k+1), 4:5])
  params <- params[1:k, ]
  params
}

…and the main function with the replication algorithm:

replicate_payoff <- function(payoff) {
  params <- calculate_params(payoff)
  suppressMessages(attach(params))
  k <- nrow(params)
  
  portfolios <- as.data.frame(matrix("", nrow = k, ncol = 6))
  colnames(portfolios) <- c("zerobonds", "nominal", "calls", "call_strike", "puts", "put_strike")
  
  # step 0 (initialization)
  i <- 1
  i_r <- 1
  i_l <- 1
  
  while (i <= k) {
    
    # step 1 (leveling)
    if (f_pi[i] != 0) {
      portfolios[i, "zerobonds"] <- s_f_pi[i]
      portfolios[i, "nominal"] <- abs(f_pi[i])
    }
    
    # step 2 (replication to the right)
    while (i_r <= k) {
      if (i_r == i) {
        if (lambda[i] != 0) {
          portfolios[i, "calls"] <- paste(portfolios[i, "calls"], lambda[i])
          portfolios[i, "call_strike"] <- paste(portfolios[i, "call_strike"], pi[i])
        }
        i_r <- i_r + 1
        next
      }
      if ((lambda[i_r] - lambda[i_r-1]) != 0) {
        portfolios[i, "calls"] <- paste(portfolios[i, "calls"], (lambda[i_r] - lambda[i_r-1]))
        portfolios[i, "call_strike"] <- paste(portfolios[i, "call_strike"], pi[i_r])
      }
      i_r <- i_r + 1
    }
        
    # step 3 (replication to the left)
    while (i_l != 1) {
      if (i_l == i) {
        if (-lambda[i_l-1] != 0) {
          portfolios[i, "puts"] <- paste(portfolios[i, "puts"], -lambda[i_l-1])
          portfolios[i, "put_strike"] <- paste(portfolios[i, "put_strike"], pi[i_l])
        }
      } else {
        if ((lambda[i_l] - lambda[i_l-1]) != 0) {
          portfolios[i, "puts"] <- paste(portfolios[i, "puts"], (lambda[i_l] - lambda[i_l-1]))
          portfolios[i, "put_strike"] <- paste(portfolios[i, "put_strike"], pi[i_l])
        }
      }
      i_l <- i_l - 1
    }

    # step 4
    i <- i + 1
    i_r <- i
    i_l <- i
  }

  # remove duplicate portfolios
  portfolios <- unique(portfolios)
  # renumber rows after removal
  row.names(portfolios) <- 1:nrow(portfolios)
  portfolios
}

Let us test our function for the plain vanilla call:

replicate_payoff(payoff)
##   zerobonds nominal calls call_strike  puts put_strike
## 1                       1         100                 
## 2         1      10     1         110  -1 1    110 100

There are always several possibilities for replication. In this case, the first is just our call with a strike of 100. Another possibility is buying a zerobond with a nominal of 10, going long a call with strike 110 and simultaneously going short a put with strike 110 and long another put with strike 100.

Let us try a more complicated payoff, a classic bear spread (which is also the example given in the paper):

payoff <- data.frame(pi = c(0, 90, 110, Inf), f_pi = c(20, 20, 0, 0))
payoff
##    pi f_pi
## 1   0   20
## 2  90   20
## 3 110    0
## 4 Inf    0

plot_payoff(payoff)

replicate_payoff(payoff)
##   zerobonds nominal calls call_strike  puts put_strike
## 1         1      20  -1 1      90 110                 
## 2                                      1 -1     110 90

Or for a so-called airbag note:

payoff <- data.frame(pi = c(0, 80, 100, 200, Inf), f_pi = c(0, 100, 100, 200, Inf))
payoff
##    pi f_pi
## 1   0    0
## 2  80  100
## 3 100  100
## 4 200  200
## 5 Inf  Inf

plot_payoff(payoff, xtrpol = 1)

replicate_payoff(payoff)
##   zerobonds nominal         calls call_strike        puts  put_strike
## 1                    1.25 -1.25 1    0 80 100                        
## 2         1     100             1         100       -1.25          80
## 3         1     200             1         200  -1 1 -1.25  200 100 80

As a final example: how to replicate the underlying itself? Let’s see:

payoff <- data.frame(pi = c(0, 100, Inf), f_pi = c(0, 100, Inf))
payoff
##    pi f_pi
## 1   0    0
## 2 100  100
## 3 Inf  Inf

plot_payoff(payoff, 1)

replicate_payoff(payoff)
##   zerobonds nominal calls call_strike puts put_strike
## 1                       1           0                
## 2         1     100     1         100   -1        100

The first solution correctly gives us what is called a zero-strike call, i.e. a call with the strike of zero!

I hope you find this helpful! If you have any questions or comments, please leave them below.

I am even thinking that it might be worthwhile to turn this into a package and put it on CRAN, yet I don’t have the time to do that at the moment… if you are interested in cooperating on that please leave a note in the comments too. Thank you!


UPDATE November 3, 2020
For another (a little bit more involved) example, see my answer on Quant.SE: Replicate a Portfolio with Given Payoff.

3 thoughts on “Financial Engineering: Static Replication of any Payoff Function”

  1. At conceptual level, this is something that an investor contemplating buying a structured product should want to have. I.e. comparing the product offer against market price for the replicating portfolio of listed instruments (which, unlike a certificate, would have no counterparty risk). But relatively few products can be fully replicated by listed derivatives, and when it is possible, one can predict the outcome beforehand. In fairness, at least some providers of institutional products make market for them at reasonable spread. I couldn’t see anything objectionable in such. Market for long-running listed derivatives has almost no liquidity, so static replication might not even be possible due to that.

    1. Thank you for your comment, Lauri.

      I don’t know how it is in other countries but in German-speaking countries, there is a huge market for structured products like certificates with all kinds of payoff functions (and all kinds of fancy names)… and often very complicated fee structures.

      The above tool should prove useful for all investors who want to look a little bit behind the scenes.

      1. Retail customers enjoy the variety in almost everything. I don’t follow our local offering very much, but my impression is that structures receive short-lived waves of retail investors’ capital and are then forgotten for five years or so. This is a small country, so salespeople can run out of prospective clients pretty quick, especially if they receive bad press coverage. Last time this happened was around 2018 (got a little story to tell about that), so I would envision their comeback in 2023, at earliest.

Leave a Reply

Your email address will not be published. Required fields are marked *

I accept that my given data and my IP address is sent to a server in the USA only for the purpose of spam prevention through the Akismet program.More information on Akismet and GDPR.

This site uses Akismet to reduce spam. Learn how your comment data is processed.