Last active
December 1, 2024 16:56
-
-
Save DavisVaughan/40a64ce354317e6e52dd3a5e9375898a to your computer and use it in GitHub Desktop.
Exploring finanicial binomial recombining trees with tidygraph
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# SCROLL TO THE GITHUB COMMENT BELOW FOR THE FULL OUTPUT WITH IMAGES | |
# COPY AND PASTE THIS FOR EASY ACCESS TO REPRODUCIBLE CODE | |
# ------------------------------------------------------------------------------ | |
library(tidygraph) | |
library(ggraph) | |
library(dplyr) | |
library(igraph) | |
library(tibble) | |
# ------------------------------------------------------------------------------ | |
# Some helper functions | |
nrow_nodes <- function(graph) { | |
nrow(as_tibble(activate(graph, "nodes"))) | |
} | |
nrow_edges <- function(graph) { | |
nrow(as_tibble(activate(graph, "edges"))) | |
} | |
# ------------------------------------------------------------------------------ | |
# You can create really basic tree structures | |
my_tree <- create_tree(40, 2, mode = "out") | |
my_tree %>% | |
mutate(lab = 1:40) %>% | |
ggraph(layout = "tree") + | |
geom_edge_link() + | |
geom_node_label(mapping = aes(label = lab)) + | |
coord_flip() + | |
scale_y_reverse() + | |
theme_void() | |
# In tidygraph, networks are represented as two tidy data frames. | |
# one is for edges (lines between labels), one is for nodes (the labels) | |
my_tree | |
# But notice how these are not recombining trees. In fact, it seems like a | |
# recombining tree is not a true "tree" as defined in graph theory, so I had | |
# to come up with my own | |
# ------------------------------------------------------------------------------ | |
create_binomial_recombining_tree <- function(depth) { | |
vec <- c() | |
sep <- 0L | |
vec_along <- vector("double", depth - 1) | |
last_val <- 1 | |
for(i in seq_len(depth - 1)) { | |
vec_along[i] <- last_val + i | |
last_val <- vec_along[i] | |
} | |
vec_along <- c(1, vec_along) | |
for(i in vec_along) { | |
sep <- sep + 1L | |
for(j in seq_len(sep)) { | |
k <- j - 1 | |
new <- c(i + k , i + sep + k, i + k, i + sep + 1 + k) | |
vec <- c(vec, new) | |
} | |
} | |
edge_list <- matrix(vec, ncol = 2, byrow = TRUE) | |
itree <- igraph::graph_from_edgelist(edge_list) | |
bin_recomb_tree <- tidygraph::as_tbl_graph(itree) | |
bin_recomb_tree | |
} | |
# ------------------------------------------------------------------------------ | |
bin_recomb_tree <- create_binomial_recombining_tree(4) | |
# Plot it! | |
bin_recomb_tree %>% | |
ggraph(layout = "tree") + | |
geom_edge_link() + | |
geom_node_label(mapping = aes(label = seq_len(nrow_nodes(bin_recomb_tree)))) + | |
coord_flip() + | |
scale_y_reverse() + | |
theme_void() | |
# This is neat, but kind of ugly. It doesn't look like our binomial tree structure | |
# because it flattens out rather than doing the up/down thing. Luckily... | |
bin_recomb_tree %>% | |
ggraph(layout = "sugiyama") + | |
geom_edge_link() + | |
geom_node_label(mapping = aes(label = seq_len(nrow_nodes(bin_recomb_tree)))) + | |
coord_flip() + | |
scale_y_reverse() + | |
theme_void() | |
# SUGIYAMA TO THE RESCUE | |
# ------------------------------------------------------------------------------ | |
# Tidygraph is useful for more than just pictures | |
# In fact, ggraph is used for the pictures, tidygraph is all about manipulation | |
# and calculations along networks | |
# This means we can do something like calculate the up / down price of a stock | |
# along each node! | |
bin_recomb_tree <- create_binomial_recombining_tree(5) | |
init <- 100 | |
up <- 1.1 | |
down <- .9 | |
# Normally this is detected automatically if it was a true "tree" | |
root_specification <- c(TRUE, rep(FALSE, nrow_nodes(bin_recomb_tree) - 1)) | |
# Below is where the magic happens | |
# map_bfs_dbl() is a variation on map() that applies the function to each node, | |
# it comes to each node in the order defined from a breadth first search | |
bin_recomb_tree_with_prices <- bin_recomb_tree %>% | |
mutate( | |
# The function accepts: | |
# path - a data frame of the path taken to the node so far. Will be used | |
# to calculate up/down movements leading up to now | |
# node - the index of the current node | |
# dist - the distance from the root node | |
prices = map_bfs_dbl(root_specification, .f = function(path, node, dist, ...) { | |
# If we are just starting, return initial stock price | |
if(nrow(path) == 0L) { | |
return(init) | |
} | |
# Modify the path by adding the current node / dist | |
# then calculate the up_down logical values (NA = start, TRUE = down, FALSE = up) | |
path_mod <- path %>% | |
add_row(node = node, dist = dist) %>% | |
mutate(ndiff = node - lag(node), | |
up_down = (dist == ndiff)) | |
up_down <- path_mod$up_down | |
# For every TRUE, replace with the down value | |
up_down[up_down == 1] <- down | |
up_down[up_down == 0] <- up | |
up_down[is.na(up_down)] <- 1 | |
# Take the product of the vector of up/down values to cumulate them | |
path_multiplier <- prod(up_down) | |
# Take the initial stock price and multiply it by the up/down path values | |
init * path_multiplier | |
}) | |
) | |
# As you can see, the prices Node Data has been filled in! | |
bin_recomb_tree_with_prices | |
# ------------------------------------------------------------------------------ | |
# Now we can look at our binomial tree! | |
bin_recomb_tree_with_prices %>% | |
mutate(pretty_prices = round(.N()$prices, 2)) %>% | |
ggraph(layout = "sugiyama") + | |
geom_edge_link() + | |
geom_node_label(mapping = aes(label = pretty_prices)) + | |
coord_flip() + | |
scale_y_reverse() + | |
theme_void() | |
# ------------------------------------------------------------------------------ | |
# you could wrap this up in a function of some kind | |
# you could imagine parameterizing around the function to be applied to the up node | |
# vs the down node or something. | |
# The beautiful thing is that now we just change the depth and we can get | |
# much larger trees! | |
# depth = 10 and then run the same code. | |
bin_recomb_tree <- create_binomial_recombining_tree(10) | |
init <- 100 | |
up <- 1.1 | |
down <- .9 | |
# Normally this is detected automatically if it was a true "tree" | |
root_specification <- c(TRUE, rep(FALSE, nrow_nodes(bin_recomb_tree) - 1)) | |
# Below is where the magic happens | |
# map_bfs_dbl() is a variation on map() that applies the function to each node, | |
# it comes to each node in the order defined from a breadth first search | |
bin_recomb_tree_with_prices_big <- bin_recomb_tree %>% | |
mutate( | |
# The function accepts: | |
# path - a data frame of the path taken to the node so far. Will be used | |
# to calculate up/down movements leading up to now | |
# node - the index of the current node | |
# dist - the distance from the root node | |
prices = map_bfs_dbl(root_specification, .f = function(path, node, dist, ...) { | |
# If we are just starting, return initial stock price | |
if(nrow(path) == 0L) { | |
return(init) | |
} | |
# Modify the path by adding the current node / dist | |
# then calculate the up_down logical values (NA = start, TRUE = down, FALSE = up) | |
path_mod <- path %>% | |
add_row(node = node, dist = dist) %>% | |
mutate(ndiff = node - lag(node), | |
up_down = (dist == ndiff)) | |
up_down <- path_mod$up_down | |
# For every TRUE, replace with the down value | |
up_down[up_down == 1] <- down | |
up_down[up_down == 0] <- up | |
up_down[is.na(up_down)] <- 1 | |
# Take the product of the vector of up/down values to cumulate them | |
path_multiplier <- prod(up_down) | |
# Take the initial stock price and multiply it by the up/down path values | |
init * path_multiplier | |
}) | |
) | |
bin_recomb_tree_with_prices_big %>% | |
mutate(pretty_prices = round(.N()$prices, 2)) %>% | |
ggraph(layout = "sugiyama") + | |
geom_edge_link() + | |
geom_node_label(mapping = aes(label = pretty_prices)) + | |
coord_flip() + | |
scale_y_reverse() + | |
theme_void() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
# SUGIYAMA TO THE RESCUE
Created on 2018-05-01 by the reprex package (v0.2.0).