Introduction

Most network studies in finance deploy quantitative data to assess the interconnectedness among firms. A common approach relies on stock data to assess the co-movement across stock prices - known as correlation-based networks. Another common approach relies on cross holdings (transactions) among entities. These interactions, hence, determine the degree of interconnectedness of firms in the system. In this vignette, I will demonstrate a simple approach to identify network structure across companies using publicly available textual data.

Getting Started

To get started, I use a number of packages. The stringdist will be used to construct a similarity metric that proxies the degree to which two documents are related. I refer to rvest package that makes parsing web data so intuitive and simple. I also rely on the tm package to do some text cleaning. Finally, I rely on two packages, igraph and visNetwork, for network visualization.

library(stringdist)
library(rvest)
library(tm)
library(igraph)
library(visNetwork)

I look at the 12 different tickers from 6 different industries: Financials, Technology, Health, Utility, Energy, and Consumer Non-Durable.

tics <- c("JPM","BAC","GOOG","AAPL","MMM","AAC","T","VZ","XOM","CVX","KO","BUD")

Using the rvest I parse the profile of each ticker from Yahoo Finance using the following function:

read_profile <- function(tic) {
  theurl <- paste("https://finance.yahoo.com/quote/",tic,"/profile?p=",tic,sep = "")
  file1 <- read_html(theurl)
  text1 <- file1 %>%
    html_nodes("section") %>%
    html_text()
  
  text1 <- text1[grep("description",text1,ignore.case = T)]
  text1 <- text1[2]
  return(text1)
  }

In case you are not familiar with rvest, check this link, for a brief introduction.

I run the read_profile over each ticker:

profile.list <- lapply(tics, read_profile)
names(profile.list) <- tics

In NLP context, the profile.list is known as the corpus, i.e. the list of all documents. Using the tm package, users can utilize a number of functions for text cleaning and stemming. In this case, I transform the documents into lower case and drop stop-words, punctuations, and numbers.

profile.list <- lapply(profile.list,tolower)
profile.list <- lapply(profile.list,function(x) removeWords(x, stopwords("english")) )
profile.list <- lapply(profile.list,removePunctuation)
profile.list <- lapply(profile.list,removeNumbers)

For instance, I can look at the unique number of words in each document:

words_u <- lapply(profile.list, function(x) unique(unlist(strsplit(x, " "))) )
words_u <- lapply(words_u, function(x) x[nchar(x)>0] )

Given the words_u list, I can count the overlapping words between each pair of firms. For instance, looking at the two banks JPM and BAC, we have a large list of overlapping words:

intersect(words_u$JPM,words_u$BAC)
 [1] "operates"     "financial"    "services"     "worldwide"   
 [5] "four"         "segments"     "consumer"     "banking"     
 [9] "investment"   "bank"         "commercial"   "asset"       
[13] "wealth"       "management"   "segment"      "offers"      
[17] "products"     "consumers"    "lending"      "solutions"   
[21] "small"        "businesses"   "residential"  "mortgages"   
[25] "home"         "equity"       "loans"        "credit"      
[29] "cards"        "leases"       "provides"     "including"   
[33] "debt"         "markets"      "well"         "treasury"    
[37] "securities"   "risk"         "brokerage"    "custody"     
[41] "corporations" "financing"    "real"         "estate"      
[45] "investors"    "money"        "market"       "retirement"  
[49] "founded"     

Whereas, if we were to compare between JPM and BUD, we find only a few common words, which are related to the establishment of the company rather than the business model:

intersect(words_u$JPM,words_u$BUD)
[1] "company"       "worldwide"     "offers"        "including"    
[5] "founded"       "headquartered"

Fuzzy Matching

To quantify the above, one can count the overlapping words among the companies. However, this approach is either “hit or miss”. To get a more robust perspective, I will use a “fuzzy” matching approach to approximate the similarity (distance) between two given documents.

Specifically, I will refer to the stringdist package that computes the distance between two strings, s1 and s2. To demonstrate this, I provide the following example:

s1 <- "Majeed loves R programming"
s2 <- "Majeed loves Sichuan food"
stringdist(s1,s2,method = "jw")
[1] 0.2421937

The stringdist takes two arguments as the strings to be compared, whereas the third argument determines the method. In this illustration, I use the Jaro–Winkler (JW) distance algorithm that returns a score between 0 and 1. If two strings are identical, the result is zero. If there is no overlap at all between the two strings, then the function returns 1. Comparing between s1 and s2, we observe that the score is around 0.24, whereas when we compare between s1 and s3, we get a lower score:

s3 <- "Majeed loves food"
stringdist(s1,s3,method = "jw")
[1] 0.2126697

While s2 and s3 are related to food than programming, the latter has a smaller number of characters that mismatches the first string. The larger the string is, the more likely to find similarities as well as dissimilarities between the two.

Text-Based Network

Given the above illustration, I will demonstrate how to use the JW algorithm to define a text-based network as follows. First, to define a network, I need to compute the adjacency matrix. In our case, the adjacency matrix will denote the similarity between the companies. If we have \(n\) firms, we need to compute \(\frac{n\times(n-1)}{2}\) similarity measures. To move forward, let us consider all possible permutations using the tickers we have

M <- data.frame(t(combn(tics,2)))
dim(M)
[1] 66  2

In our case, there are 66 matches, i.e. \(12\times11 \div 2\). For each combo, I will compute the distance between the two using the JW algorithm:

M$D <- apply(M,1,function(x) stringdist(profile.list[[x[1]]],profile.list[[x[2]]],method = "jw")   )
head(M,11)

Not surprisingly, we observe that the smallest distance for JPM is BAC, whereas the largest distance is when compared with BUD.

I define the adjacency matrix using the distance D. To do so, I do the following steps:

n <- length(tics)
W <- matrix(NA,n,n)
rownames(W) <- colnames(W) <- tics
W[lower.tri(W)] <- M$D
W[upper.tri(W)] <- t(W)[upper.tri(W)]

Second, I use a cutoff point to identify significant links among firms. In this case, I use an arbitrary choice of 0.25 as the cutoff point. Additionally, I transform the distance into similarity score using a logit transformation:

W[W > 0.25] <- NA
logit <- function(p) log(p)/log(1-p)
W <- logit(W)
W[is.na(W)] <- 0
data.frame(W)

We see that BUD has no neighbors as its column indicates.

Finally, given the adjacency matrix W, I can produce a network graph using the following commands (see this link by Katya Ognyanova, for an excellent summary on static and dynamic network visualization)

WW=graph.adjacency(W,diag=TRUE,weighted = TRUE,mode = "undirected" ) 
data <- toVisNetworkData(WW)
# get the edges/links
vis.links <- data$edges
vis.links$value <- log(vis.links$weight)
# get the nodes
vis.nodes <- data$nodes
vis.nodes$label  <- vis.nodes$label 
vis.nodes$font.size  <-30
vis.nodes$font.color <- "black"
# add 6 different colors to highlight industries
pal <- colorRampPalette(c("yellow","blue"))
cols <- sort(rep(pal(n/2),2))
vis.nodes$color.background <- cols
vis.nodes$color.border <- "black"
vis.nodes$color.highlight.border <- "darkred"
# finally, visualize the network
Net <- visNetwork(vis.nodes, vis.links) %>%
  visOptions(highlightNearest = T) %>%
  visLayout(randomSeed = 11)  %>% 
  visPhysics(stabilization = FALSE)
  # %>% visIgraphLayout(layout = "layout_with_fr")
Net %>% visSave(file = "Net.html")

I refer to the visNetwork to construct an interactive network. However, I also utilize the commands from igraph to derive the adjacency matrix, which I feed into the former. Finally, I save the visNetwork as Net into a HTML file to control its location and size. I load the network using the htmltools package:

htmltools::includeHTML("Net.html")
visNetwork

The above network is dynamic, allowing users to highlight different nodes and clusters. The thickness of the edge between the nodes indicates the similarity level between the firms. For instance, we see that the thickest edge is the one between JPM and BAC. The colors of the nodes indicate the industry that the firms belong to. In most cases, we observe that companies in the same industry form a network, except the case for Google and Apple as well as Budweiser, which seems to be isolated from all other firms.

Summary

This vignette provides a simple illustration on how to quantify textual data and, hence, form text-based network. The approach taken here can be generalized using other advanced text mining techniques(see e.g., word2vec). Additionally, users can refer to richer sources of textual data, such as SEC EDGAR, to gain a more detailed perspective on companies’ business models.


Email | Linkedin | Github

LS0tCnRpdGxlOiAiQSBUZXh0LUJhc2VkIE5ldHdvcmsiCm91dHB1dDogaHRtbF9ub3RlYm9vawojb3V0cHV0OiBybWFya2Rvd246OmdpdGh1Yl9kb2N1bWVudAojICBwZGZfZG9jdW1lbnQ6IGRlZmF1bHQKIyBvdXRwdXQ6CiMgICBodG1sX25vdGVib29rOiBkZWZhdWx0CiMgICBwZGZfZG9jdW1lbnQ6IGRlZmF1bHQKYXV0aG9yOiBNYWplZWQgU2ltYWFuCmRhdGU6IEF1Z3VzdCA1LCAyMDE4Ci0tLQo8IS0tIDxzdHlsZT4gLS0+CjwhLS0gLm1haW4tY29udGFpbmVyIHsgLS0+CjwhLS0gICBtYXgtd2lkdGg6IG5vbmU7IWltcG9ydGFudCAtLT4KPCEtLSAgIG1heC1oZWlnaHQ6IG5vbmU7IWltcG9ydGFudCAtLT4KPCEtLSAgIG1hcmdpbi1sZWZ0OiBhdXRvOyFpbXBvcnRhbnQgLS0+CjwhLS0gICBtYXJnaW4tcmlnaHQ6IGF1dG87IWltcG9ydGFudCAtLT4KPCEtLSB9IC0tPgo8IS0tIDwvc3R5bGU+IC0tPgoKCiMjIyBJbnRyb2R1Y3Rpb24KTW9zdCBuZXR3b3JrIHN0dWRpZXMgaW4gZmluYW5jZSBkZXBsb3kgcXVhbnRpdGF0aXZlIGRhdGEgdG8gYXNzZXNzIHRoZSBpbnRlcmNvbm5lY3RlZG5lc3MgYW1vbmcgZmlybXMuIEEgY29tbW9uIGFwcHJvYWNoIHJlbGllcyBvbiBzdG9jayBkYXRhIHRvIGFzc2VzcyB0aGUgY28tbW92ZW1lbnQgYWNyb3NzIHN0b2NrIHByaWNlcyAtIGtub3duIGFzIGNvcnJlbGF0aW9uLWJhc2VkIG5ldHdvcmtzLiBBbm90aGVyIGNvbW1vbiBhcHByb2FjaCByZWxpZXMgb24gY3Jvc3MgaG9sZGluZ3MgKHRyYW5zYWN0aW9ucykgYW1vbmcgZW50aXRpZXMuIFRoZXNlIGludGVyYWN0aW9ucywgaGVuY2UsIGRldGVybWluZSB0aGUgZGVncmVlIG9mIGludGVyY29ubmVjdGVkbmVzcyBvZiBmaXJtcyBpbiB0aGUgc3lzdGVtLiAgSW4gdGhpcyB2aWduZXR0ZSwgSSB3aWxsIGRlbW9uc3RyYXRlIGEgc2ltcGxlIGFwcHJvYWNoIHRvIGlkZW50aWZ5IG5ldHdvcmsgc3RydWN0dXJlIGFjcm9zcyBjb21wYW5pZXMgdXNpbmcgcHVibGljbHkgYXZhaWxhYmxlIHRleHR1YWwgZGF0YS4gIAoKIyMjIEdldHRpbmcgU3RhcnRlZApUbyBnZXQgc3RhcnRlZCwgSSB1c2UgYSBudW1iZXIgb2YgcGFja2FnZXMuIFRoZSBgc3RyaW5nZGlzdGAgd2lsbCBiZSB1c2VkIHRvIGNvbnN0cnVjdCBhIHNpbWlsYXJpdHkgbWV0cmljIHRoYXQgcHJveGllcyB0aGUgZGVncmVlIHRvIHdoaWNoIHR3byBkb2N1bWVudHMgYXJlIHJlbGF0ZWQuIEkgcmVmZXIgdG8gYHJ2ZXN0YCBwYWNrYWdlIHRoYXQgbWFrZXMgcGFyc2luZyB3ZWIgZGF0YSBzbyBpbnR1aXRpdmUgYW5kIHNpbXBsZS4gSSBhbHNvIHJlbHkgb24gdGhlIGB0bWAgcGFja2FnZSB0byBkbyBzb21lIHRleHQgY2xlYW5pbmcuIEZpbmFsbHksIEkgcmVseSBvbiB0d28gcGFja2FnZXMsIGBpZ3JhcGhgIGFuZCBgdmlzTmV0d29ya2AsIGZvciBuZXR3b3JrIHZpc3VhbGl6YXRpb24uIApgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpsaWJyYXJ5KHN0cmluZ2Rpc3QpCmxpYnJhcnkocnZlc3QpCmxpYnJhcnkodG0pCmxpYnJhcnkoaWdyYXBoKQpsaWJyYXJ5KHZpc05ldHdvcmspCmBgYAoKSSBsb29rIGF0IHRoZSAxMiBkaWZmZXJlbnQgdGlja2VycyBmcm9tIDYgZGlmZmVyZW50IGluZHVzdHJpZXM6IEZpbmFuY2lhbHMsIFRlY2hub2xvZ3ksIEhlYWx0aCwgVXRpbGl0eSwgRW5lcmd5LCBhbmQgQ29uc3VtZXIgTm9uLUR1cmFibGUuCmBgYHtyfQp0aWNzIDwtIGMoIkpQTSIsIkJBQyIsIkdPT0ciLCJBQVBMIiwiTU1NIiwiQUFDIiwiVCIsIlZaIiwiWE9NIiwiQ1ZYIiwiS08iLCJCVUQiKQpgYGAKClVzaW5nIHRoZSBgcnZlc3RgIEkgcGFyc2UgdGhlIHByb2ZpbGUgb2YgZWFjaCB0aWNrZXIgZnJvbSBZYWhvbyBGaW5hbmNlIHVzaW5nIHRoZSBmb2xsb3dpbmcgZnVuY3Rpb246CmBgYHtyfQpyZWFkX3Byb2ZpbGUgPC0gZnVuY3Rpb24odGljKSB7CiAgdGhldXJsIDwtIHBhc3RlKCJodHRwczovL2ZpbmFuY2UueWFob28uY29tL3F1b3RlLyIsdGljLCIvcHJvZmlsZT9wPSIsdGljLHNlcCA9ICIiKQogIGZpbGUxIDwtIHJlYWRfaHRtbCh0aGV1cmwpCiAgdGV4dDEgPC0gZmlsZTEgJT4lCiAgICBodG1sX25vZGVzKCJzZWN0aW9uIikgJT4lCiAgICBodG1sX3RleHQoKQogIAogIHRleHQxIDwtIHRleHQxW2dyZXAoImRlc2NyaXB0aW9uIix0ZXh0MSxpZ25vcmUuY2FzZSA9IFQpXQogIHRleHQxIDwtIHRleHQxWzJdCgogIHJldHVybih0ZXh0MSkKICB9CmBgYApJbiBjYXNlIHlvdSBhcmUgbm90IGZhbWlsaWFyIHdpdGggYHJ2ZXN0YCwgY2hlY2sgdGhpcyBbbGlua10oaHR0cHM6Ly9ibG9nLnJzdHVkaW8uY29tLzIwMTQvMTEvMjQvcnZlc3QtZWFzeS13ZWItc2NyYXBpbmctd2l0aC1yLyksIGZvciBhIGJyaWVmIGludHJvZHVjdGlvbi4gCgpJIHJ1biB0aGUgYHJlYWRfcHJvZmlsZWAgb3ZlciBlYWNoIHRpY2tlcjoKYGBge3J9CnByb2ZpbGUubGlzdCA8LSBsYXBwbHkodGljcywgcmVhZF9wcm9maWxlKQpuYW1lcyhwcm9maWxlLmxpc3QpIDwtIHRpY3MKYGBgCkluIE5MUCBjb250ZXh0LCB0aGUgYHByb2ZpbGUubGlzdGAgaXMga25vd24gYXMgdGhlIGNvcnB1cywgaS5lLiB0aGUgbGlzdCBvZiBhbGwgZG9jdW1lbnRzLiBVc2luZyB0aGUgYHRtYCBwYWNrYWdlLCB1c2VycyBjYW4gdXRpbGl6ZSBhIG51bWJlciBvZiBmdW5jdGlvbnMgZm9yIHRleHQgY2xlYW5pbmcgYW5kIHN0ZW1taW5nLiBJbiB0aGlzIGNhc2UsIEkgdHJhbnNmb3JtIHRoZSBkb2N1bWVudHMgaW50byBsb3dlciBjYXNlIGFuZCBkcm9wIHN0b3Atd29yZHMsIHB1bmN0dWF0aW9ucywgYW5kIG51bWJlcnMuCmBgYHtyfQpwcm9maWxlLmxpc3QgPC0gbGFwcGx5KHByb2ZpbGUubGlzdCx0b2xvd2VyKQpwcm9maWxlLmxpc3QgPC0gbGFwcGx5KHByb2ZpbGUubGlzdCxmdW5jdGlvbih4KSByZW1vdmVXb3Jkcyh4LCBzdG9wd29yZHMoImVuZ2xpc2giKSkgKQpwcm9maWxlLmxpc3QgPC0gbGFwcGx5KHByb2ZpbGUubGlzdCxyZW1vdmVQdW5jdHVhdGlvbikKcHJvZmlsZS5saXN0IDwtIGxhcHBseShwcm9maWxlLmxpc3QscmVtb3ZlTnVtYmVycykKCmBgYApGb3IgaW5zdGFuY2UsIEkgY2FuIGxvb2sgYXQgdGhlIHVuaXF1ZSBudW1iZXIgb2Ygd29yZHMgaW4gZWFjaCBkb2N1bWVudDogCmBgYHtyfQp3b3Jkc191IDwtIGxhcHBseShwcm9maWxlLmxpc3QsIGZ1bmN0aW9uKHgpIHVuaXF1ZSh1bmxpc3Qoc3Ryc3BsaXQoeCwgIiAiKSkpICkKd29yZHNfdSA8LSBsYXBwbHkod29yZHNfdSwgZnVuY3Rpb24oeCkgeFtuY2hhcih4KT4wXSApCmBgYApHaXZlbiB0aGUgYHdvcmRzX3VgIGxpc3QsIEkgY2FuIGNvdW50IHRoZSBvdmVybGFwcGluZyB3b3JkcyBiZXR3ZWVuIGVhY2ggcGFpciBvZiBmaXJtcy4gRm9yIGluc3RhbmNlLCBsb29raW5nIGF0IHRoZSB0d28gYmFua3MgSlBNIGFuZCBCQUMsIHdlIGhhdmUgYSBsYXJnZSBsaXN0IG9mIG92ZXJsYXBwaW5nIHdvcmRzOgpgYGB7cn0KaW50ZXJzZWN0KHdvcmRzX3UkSlBNLHdvcmRzX3UkQkFDKQpgYGAKV2hlcmVhcywgaWYgd2Ugd2VyZSB0byBjb21wYXJlIGJldHdlZW4gSlBNIGFuZCBCVUQsIHdlIGZpbmQgb25seSBhIGZldyBjb21tb24gd29yZHMsIHdoaWNoIGFyZSByZWxhdGVkIHRvIHRoZSBlc3RhYmxpc2htZW50IG9mIHRoZSBjb21wYW55IHJhdGhlciB0aGFuIHRoZSBidXNpbmVzcyBtb2RlbDoKYGBge3J9CmludGVyc2VjdCh3b3Jkc191JEpQTSx3b3Jkc191JEJVRCkKYGBgCgojIyMgRnV6enkgTWF0Y2hpbmcKVG8gcXVhbnRpZnkgdGhlIGFib3ZlLCBvbmUgY2FuIGNvdW50IHRoZSBvdmVybGFwcGluZyB3b3JkcyBhbW9uZyB0aGUgY29tcGFuaWVzLiBIb3dldmVyLCB0aGlzIGFwcHJvYWNoIGlzIGVpdGhlciAiaGl0IG9yIG1pc3MiLiBUbyBnZXQgYSBtb3JlIHJvYnVzdCBwZXJzcGVjdGl2ZSwgSSB3aWxsIHVzZSBhICJmdXp6eSIgbWF0Y2hpbmcgYXBwcm9hY2ggdG8gYXBwcm94aW1hdGUgdGhlIHNpbWlsYXJpdHkgKGRpc3RhbmNlKSBiZXR3ZWVuIHR3byBnaXZlbiBkb2N1bWVudHMuCgpTcGVjaWZpY2FsbHksIEkgd2lsbCByZWZlciB0byB0aGUgYHN0cmluZ2Rpc3RgIHBhY2thZ2UgdGhhdCBjb21wdXRlcyB0aGUgZGlzdGFuY2UgYmV0d2VlbiB0d28gc3RyaW5ncywgYHMxYCBhbmQgYHMyYC4gVG8gZGVtb25zdHJhdGUgdGhpcywgSSBwcm92aWRlIHRoZSBmb2xsb3dpbmcgZXhhbXBsZToKYGBge3J9CnMxIDwtICJNYWplZWQgbG92ZXMgUiBwcm9ncmFtbWluZyIKczIgPC0gIk1hamVlZCBsb3ZlcyBTaWNodWFuIGZvb2QiCnN0cmluZ2Rpc3QoczEsczIsbWV0aG9kID0gImp3IikKYGBgClRoZSBgc3RyaW5nZGlzdGAgdGFrZXMgdHdvIGFyZ3VtZW50cyBhcyB0aGUgc3RyaW5ncyB0byBiZSBjb21wYXJlZCwgd2hlcmVhcyB0aGUgdGhpcmQgYXJndW1lbnQgZGV0ZXJtaW5lcyB0aGUgbWV0aG9kLiBJbiB0aGlzIGlsbHVzdHJhdGlvbiwgSSB1c2UgdGhlIEphcm/igJNXaW5rbGVyIChKVykgZGlzdGFuY2UgYWxnb3JpdGhtIHRoYXQgcmV0dXJucyBhIHNjb3JlIGJldHdlZW4gMCBhbmQgMS4gSWYgdHdvIHN0cmluZ3MgYXJlIGlkZW50aWNhbCwgdGhlIHJlc3VsdCBpcyB6ZXJvLiBJZiB0aGVyZSBpcyBubyBvdmVybGFwIGF0IGFsbCBiZXR3ZWVuIHRoZSB0d28gc3RyaW5ncywgdGhlbiB0aGUgZnVuY3Rpb24gcmV0dXJucyAxLiBDb21wYXJpbmcgYmV0d2VlbiBgczFgIGFuZCBgczJgLCB3ZSBvYnNlcnZlIHRoYXQgdGhlIHNjb3JlIGlzIGFyb3VuZCAwLjI0LCB3aGVyZWFzIHdoZW4gd2UgY29tcGFyZSBiZXR3ZWVuIGBzMWAgYW5kIGBzM2AsIHdlIGdldCBhIGxvd2VyIHNjb3JlOgoKYGBge3J9CnMzIDwtICJNYWplZWQgbG92ZXMgZm9vZCIKc3RyaW5nZGlzdChzMSxzMyxtZXRob2QgPSAianciKQpgYGAKV2hpbGUgYHMyYCBhbmQgYHMzYCBhcmUgcmVsYXRlZCB0byBmb29kIHRoYW4gcHJvZ3JhbW1pbmcsIHRoZSBsYXR0ZXIgaGFzIGEgc21hbGxlciBudW1iZXIgb2YgY2hhcmFjdGVycyB0aGF0IG1pc21hdGNoZXMgdGhlIGZpcnN0IHN0cmluZy4gVGhlIGxhcmdlciB0aGUgc3RyaW5nIGlzLCB0aGUgbW9yZSBsaWtlbHkgdG8gZmluZCBzaW1pbGFyaXRpZXMgYXMgd2VsbCBhcyBkaXNzaW1pbGFyaXRpZXMgYmV0d2VlbiB0aGUgdHdvLiAKCiAKIyMjIFRleHQtQmFzZWQgTmV0d29yawpHaXZlbiB0aGUgYWJvdmUgaWxsdXN0cmF0aW9uLCBJIHdpbGwgZGVtb25zdHJhdGUgaG93IHRvIHVzZSB0aGUgSlcgYWxnb3JpdGhtIHRvIGRlZmluZSBhIHRleHQtYmFzZWQgbmV0d29yayBhcyBmb2xsb3dzLiBGaXJzdCwgdG8gZGVmaW5lIGEgbmV0d29yaywgSSBuZWVkIHRvIGNvbXB1dGUgdGhlIGFkamFjZW5jeSBtYXRyaXguIEluIG91ciBjYXNlLCB0aGUgYWRqYWNlbmN5IG1hdHJpeCB3aWxsIGRlbm90ZSB0aGUgc2ltaWxhcml0eSBiZXR3ZWVuIHRoZSBjb21wYW5pZXMuIElmIHdlIGhhdmUgJG4kIGZpcm1zLCB3ZSBuZWVkIHRvIGNvbXB1dGUgJFxmcmFje25cdGltZXMobi0xKX17Mn0kIHNpbWlsYXJpdHkgbWVhc3VyZXMuIFRvIG1vdmUgZm9yd2FyZCwgbGV0IHVzIGNvbnNpZGVyIGFsbCBwb3NzaWJsZSBwZXJtdXRhdGlvbnMgdXNpbmcgdGhlIHRpY2tlcnMgd2UgaGF2ZQoKYGBge3J9Ck0gPC0gZGF0YS5mcmFtZSh0KGNvbWJuKHRpY3MsMikpKQpkaW0oTSkKYGBgCkluIG91ciBjYXNlLCB0aGVyZSBhcmUgNjYgbWF0Y2hlcywgaS5lLiAkMTJcdGltZXMxMSBcZGl2IDIkLiBGb3IgZWFjaCBjb21ibywgSSB3aWxsIGNvbXB1dGUgdGhlIGRpc3RhbmNlIGJldHdlZW4gdGhlIHR3byB1c2luZyB0aGUgSlcgYWxnb3JpdGhtOgpgYGB7cn0KTSREIDwtIGFwcGx5KE0sMSxmdW5jdGlvbih4KSBzdHJpbmdkaXN0KHByb2ZpbGUubGlzdFtbeFsxXV1dLHByb2ZpbGUubGlzdFtbeFsyXV1dLG1ldGhvZCA9ICJqdyIpICAgKQpoZWFkKE0sMTEpCmBgYApOb3Qgc3VycHJpc2luZ2x5LCB3ZSBvYnNlcnZlIHRoYXQgdGhlIHNtYWxsZXN0IGRpc3RhbmNlIGZvciBKUE0gIGlzIEJBQywgd2hlcmVhcyB0aGUgbGFyZ2VzdCBkaXN0YW5jZSAgaXMgd2hlbiBjb21wYXJlZCB3aXRoIEJVRC4gCgpJIGRlZmluZSB0aGUgYWRqYWNlbmN5IG1hdHJpeCB1c2luZyB0aGUgZGlzdGFuY2UgYERgLiBUbyBkbyBzbywgSSBkbyB0aGUgZm9sbG93aW5nIHN0ZXBzOgpgYGB7cn0KbiA8LSBsZW5ndGgodGljcykKVyA8LSBtYXRyaXgoTkEsbixuKQpyb3duYW1lcyhXKSA8LSBjb2xuYW1lcyhXKSA8LSB0aWNzCldbbG93ZXIudHJpKFcpXSA8LSBNJEQKV1t1cHBlci50cmkoVyldIDwtIHQoVylbdXBwZXIudHJpKFcpXQpgYGAKU2Vjb25kLCBJIHVzZSBhIGN1dG9mZiBwb2ludCB0byBpZGVudGlmeSBzaWduaWZpY2FudCBsaW5rcyBhbW9uZyBmaXJtcy4gSW4gdGhpcyBjYXNlLCBJIHVzZSBhbiBhcmJpdHJhcnkgY2hvaWNlIG9mIDAuMjUgYXMgdGhlIGN1dG9mZiBwb2ludC4gQWRkaXRpb25hbGx5LCBJIHRyYW5zZm9ybSAgdGhlIGRpc3RhbmNlIGludG8gc2ltaWxhcml0eSBzY29yZSB1c2luZyBhIGxvZ2l0IHRyYW5zZm9ybWF0aW9uOgpgYGB7cn0KV1tXID4gMC4yNV0gPC0gTkEKbG9naXQgPC0gZnVuY3Rpb24ocCkgbG9nKHApL2xvZygxLXApClcgPC0gbG9naXQoVykKV1tpcy5uYShXKV0gPC0gMApkYXRhLmZyYW1lKFcpCmBgYApXZSBzZWUgdGhhdCBCVUQgaGFzIG5vIG5laWdoYm9ycyBhcyBpdHMgY29sdW1uIGluZGljYXRlcy4gCgpGaW5hbGx5LCBnaXZlbiB0aGUgYWRqYWNlbmN5IG1hdHJpeCBgV2AsIEkgY2FuIHByb2R1Y2UgYSBuZXR3b3JrIGdyYXBoIHVzaW5nIHRoZSBmb2xsb3dpbmcgY29tbWFuZHMgKHNlZSB0aGlzIFtsaW5rXSgiaHR0cDovL2thdGV0by5uZXQvbmV0d29yay12aXN1YWxpemF0aW9uIikgYnkgS2F0eWEgT2dueWFub3ZhLCBmb3IgYW4gZXhjZWxsZW50IHN1bW1hcnkgb24gc3RhdGljIGFuZCBkeW5hbWljIG5ldHdvcmsgdmlzdWFsaXphdGlvbikKYGBge3J9CldXPWdyYXBoLmFkamFjZW5jeShXLGRpYWc9VFJVRSx3ZWlnaHRlZCA9IFRSVUUsbW9kZSA9ICJ1bmRpcmVjdGVkIiApIApkYXRhIDwtIHRvVmlzTmV0d29ya0RhdGEoV1cpCgojIGdldCB0aGUgZWRnZXMvbGlua3MKdmlzLmxpbmtzIDwtIGRhdGEkZWRnZXMKdmlzLmxpbmtzJHZhbHVlIDwtIGxvZyh2aXMubGlua3Mkd2VpZ2h0KQoKIyBnZXQgdGhlIG5vZGVzCnZpcy5ub2RlcyA8LSBkYXRhJG5vZGVzCnZpcy5ub2RlcyRsYWJlbCAgPC0gdmlzLm5vZGVzJGxhYmVsIAp2aXMubm9kZXMkZm9udC5zaXplICA8LTMwCnZpcy5ub2RlcyRmb250LmNvbG9yIDwtICJibGFjayIKCiMgYWRkIDYgZGlmZmVyZW50IGNvbG9ycyB0byBoaWdobGlnaHQgaW5kdXN0cmllcwpwYWwgPC0gY29sb3JSYW1wUGFsZXR0ZShjKCJ5ZWxsb3ciLCJibHVlIikpCmNvbHMgPC0gc29ydChyZXAocGFsKG4vMiksMikpCnZpcy5ub2RlcyRjb2xvci5iYWNrZ3JvdW5kIDwtIGNvbHMKdmlzLm5vZGVzJGNvbG9yLmJvcmRlciA8LSAiYmxhY2siCnZpcy5ub2RlcyRjb2xvci5oaWdobGlnaHQuYm9yZGVyIDwtICJkYXJrcmVkIgoKIyBmaW5hbGx5LCB2aXN1YWxpemUgdGhlIG5ldHdvcmsKTmV0IDwtIHZpc05ldHdvcmsodmlzLm5vZGVzLCB2aXMubGlua3MpICU+JQogIHZpc09wdGlvbnMoaGlnaGxpZ2h0TmVhcmVzdCA9IFQpICU+JQogIHZpc0xheW91dChyYW5kb21TZWVkID0gMTEpICAlPiUgCiAgdmlzUGh5c2ljcyhzdGFiaWxpemF0aW9uID0gRkFMU0UpCiAgIyAlPiUgdmlzSWdyYXBoTGF5b3V0KGxheW91dCA9ICJsYXlvdXRfd2l0aF9mciIpCk5ldCAlPiUgdmlzU2F2ZShmaWxlID0gIk5ldC5odG1sIikKYGBgCgpJIHJlZmVyIHRvIHRoZSBgdmlzTmV0d29ya2AgdG8gY29uc3RydWN0IGFuIGludGVyYWN0aXZlIG5ldHdvcmsuIEhvd2V2ZXIsIEkgYWxzbyB1dGlsaXplIHRoZSBjb21tYW5kcyBmcm9tIGBpZ3JhcGhgIHRvIGRlcml2ZSB0aGUgYWRqYWNlbmN5IG1hdHJpeCwgd2hpY2ggSSBmZWVkIGludG8gdGhlIGZvcm1lci4gRmluYWxseSwgSSBzYXZlIHRoZSBgdmlzTmV0d29ya2AgYXMgYE5ldGAgaW50byBhIEhUTUwgZmlsZSB0byBjb250cm9sIGl0cyBsb2NhdGlvbiBhbmQgc2l6ZS4gSSBsb2FkIHRoZSBuZXR3b3JrIHVzaW5nIHRoZSBgaHRtbHRvb2xzYCBwYWNrYWdlOiAKYGBge3J9Cmh0bWx0b29sczo6aW5jbHVkZUhUTUwoIk5ldC5odG1sIikKYGBgCgpUaGUgYWJvdmUgbmV0d29yayBpcyBkeW5hbWljLCBhbGxvd2luZyB1c2VycyB0byBoaWdobGlnaHQgIGRpZmZlcmVudCBub2RlcyBhbmQgY2x1c3RlcnMuIFRoZSB0aGlja25lc3Mgb2YgdGhlIGVkZ2UgYmV0d2VlbiB0aGUgbm9kZXMgaW5kaWNhdGVzIHRoZSBzaW1pbGFyaXR5IGxldmVsIGJldHdlZW4gdGhlIGZpcm1zLiBGb3IgaW5zdGFuY2UsIHdlIHNlZSB0aGF0IHRoZSB0aGlja2VzdCBlZGdlIGlzIHRoZSBvbmUgYmV0d2VlbiBKUE0gYW5kIEJBQy4gVGhlIGNvbG9ycyBvZiB0aGUgbm9kZXMgaW5kaWNhdGUgdGhlIGluZHVzdHJ5IHRoYXQgdGhlIGZpcm1zIGJlbG9uZyB0by4gSW4gbW9zdCBjYXNlcywgd2Ugb2JzZXJ2ZSB0aGF0IGNvbXBhbmllcyBpbiB0aGUgc2FtZSBpbmR1c3RyeSBmb3JtIGEgbmV0d29yaywgZXhjZXB0IHRoZSBjYXNlIGZvciBHb29nbGUgYW5kIEFwcGxlIGFzIHdlbGwgYXMgQnVkd2Vpc2VyLCB3aGljaCBzZWVtcyB0byBiZSBpc29sYXRlZCBmcm9tIGFsbCBvdGhlciBmaXJtcy4gCgoKIyMjIFN1bW1hcnkKVGhpcyB2aWduZXR0ZSBwcm92aWRlcyBhIHNpbXBsZSBpbGx1c3RyYXRpb24gb24gaG93IHRvIHF1YW50aWZ5IHRleHR1YWwgZGF0YSBhbmQsIGhlbmNlLCBmb3JtIHRleHQtYmFzZWQgbmV0d29yay4gVGhlIGFwcHJvYWNoIHRha2VuIGhlcmUgY2FuIGJlIGdlbmVyYWxpemVkIHVzaW5nIG90aGVyIGFkdmFuY2VkIHRleHQgbWluaW5nIHRlY2huaXF1ZXMoc2VlIGUuZy4sICAqKndvcmQydmVjKiopLiBBZGRpdGlvbmFsbHksIHVzZXJzIGNhbiByZWZlciB0byByaWNoZXIgc291cmNlcyBvZiB0ZXh0dWFsIGRhdGEsIHN1Y2ggYXMgU0VDIEVER0FSLCB0byBnYWluIGEgbW9yZSBkZXRhaWxlZCBwZXJzcGVjdGl2ZSBvbiBjb21wYW5pZXMnIGJ1c2luZXNzIG1vZGVscy4KCgoqKiogCltFbWFpbF0obXNpbWFhbkBzdGV2ZW5zLmVkdSkgfCBbTGlua2VkaW5dKGh0dHBzOi8vd3d3LmxpbmtlZGluLmNvbS9pbi9tYWplZWQtc2ltYWFuLTg1MzgzMDQ1KSB8IFtHaXRodWJdKGh0dHBzOi8vZ2l0aHViLmNvbS9zaW1hYW44NCkKCg==