vignettes/shiny-and-rrofex.Rmd
shiny-and-rrofex.Rmd
This post is not an attempt to explain in detail how to build a Shiny application, rather it is to show a few interesting things that one can build using Shiny in conjuction with the rRofex library.
I hope that this serves as a starting point and an insipiration for others to build their own solutions.
The full application can be run either locally using a Gist from GitHub or one can access the online version:
shiny::runGist(gist = "https://gist.github.com/augustohassel/4eea614f80a8bbc548b2b4c3c5edd7c3")
The main features of this application are:
I will describe briefly each feature pointing out some relevant code for a better understanding.
At the very begining of the server function one can found a reactive value called global_connection
. Each session will have it’s own connection, so we don’t have to worry about mixing information across sessions.
global_connection <- reactiveValues(conn = NULL)
Once we login using the trading_login() function, the created object will be stored inside global_connection$conn
.
withCallingHandlers({
global_connection$conn <- trading_login(username = input$username, password = input$password, base_url = input$base_url)
},
message = function(m) {output$console_login <- renderPrint({m$message})},
warning = function(m) {output$console_login <- renderPrint({m$message})}
)
Every connection object will be destroyed at the end of each session.
In the same maner as with th login, here we start by creating a reactive value that will contain the final market data, called global_graficos
, and an environment created with rlang::env()
in which we will store each websocket element, namely the connection itself and the pre procesed market data.
global_graficos <- reactiveValues(data = NULL)
environment_graficos_iniciar <- rlang::env()
When pressing ‘Iniciar’ two things happen on the server:
observeEvent(input$graficos_iniciar, {
if (is.null(global_connection$conn)) {
sendSweetAlert(session = session, title = HTML("Mmm..."), text = HTML("Tenes que conectarte primero..."), type = "warning", html = TRUE)
} else {
trading_ws_md(connection = global_connection$conn,
destination = "data",
symbol = input$graficos_producto,
entries = list("LA"),
listen_to = list("LA_price"),
where_is_env = environment_graficos_iniciar)
global_graficos$data <- reactivePoll(intervalMillis = 1000,
session = session,
checkFunc = function() {
if (!is.null(environment_graficos_iniciar$data)) max(environment_graficos_iniciar$data$timestamp)
},
valueFunc = function() {
return(environment_graficos_iniciar$data)
})
}
})
It is important to notice that we are explicitly using the parameter where_is_env from trading_ws_md()
function and we are poiting it out to our newly created environment. If we weren’t using this parameter we would have a mixup of data between sessions because each one would be overwriting the global environment. In this away every sesions starts it’s own environment for the websocket elements.
For more information about scoping rules I strongly suggest this reading: Scoping rules for Shiny apps.
Once data it’s being acquired we will build and update the plot using the plotly
library.
output$graficos_grafico <- renderPlotly({
shiny::validate(
need(is.reactive(global_graficos$data) &&
!is.null(global_graficos$data()) &&
nrow(global_graficos$data()) > 0,
message = "Aún no hay data.")
)
plot_ly(data = global_graficos$data(),
x = ~LA_date,
y = ~LA_price,
mode = 'line') %>%
layout(title = input$graficos_producto,
xaxis = list(title = "Timestamp"),
yaxis = list(title = "Precio"))
})
We start by defining what’s the CCL on a function called outside the server scope.
The CCL is a foreign exchange ratio. It basically tells us how many pesos do we need do buy a single dollar, this by calculating the ratio of the local instrument price and its foreign complement.
ccl <- function(connection, data) {
message(glue("Busco la market data de los {n} productos locales y extranjeros...", n = nrow(data)))
precio_local <- map_df(.x = as.list(data$Local), .f = ~ trading_md(connection = connection, symbol = .x, entries = list("LA", "BI", "OF")))
precio_extranjero <- rownames_to_column(getQuote(Symbols = data$Extranjero), var = "Symbol") %>% rename(TradeTime = `Trade Time`)
message(glue("Uno las tablas y calculo el CCL.."))
data <- data %>%
left_join(precio_local %>% select(Symbol, LA_date, LA_price, BI_price, OF_price), by = c("Local" = "Symbol")) %>%
left_join(precio_extranjero %>% select(Symbol, TradeTime, Last), by = c("Extranjero" = "Symbol")) %>%
mutate(
CCL_Last = (LA_price / Last) * Factor,
CCL_Bid = (BI_price / Last) * Factor,
CCL_Offer = (OF_price / Last) * Factor
) %>%
mutate(across(.cols = starts_with("CCL_"), .fns = ~ ifelse(. == 0, NA_real_, .))) %>%
mutate(
CCL_Last = case_when(
CCL_Last %in% boxplot.stats(x = .$CCL_Last, coef = 3)$out ~ NA_real_,
TRUE ~ CCL_Last
)
)
return(data)
}
We are obtaining local market data with the function trading_md() and foreign data it’s being queried with quantomod::getQuote()
. As you have probably noticed, there’s a convertion factor that’s beeing appliead to the ratio, this has been defined manually on a dataframe outside the server scope.
Once we have selected the instruments and press ‘Iniciar’, the calculation starts and we are going to see something like this:
A bit of a hack has been made here in order to update the data:
observe({
invalidateLater(millis = input$table_ccl_timer * 1000, session = session)
if (input$table_ccl_status == TRUE) {
shinyjs::click("table_ccl_iniciar")
}
})
On this observe we are programmatically clicking the ‘Iniciar’ button with the selected frequency.
The difference from REST and websocket protcol that I would like to address is that the first one has a uni-directional nature, while the latter has a bi-direction approach. This meaning that we can open a websocket connection and wait for new data to update automatically our feed, while with REST we will have to constantly be querying over and over again if it has been any changes on the data.
The use of websocket in R it’s not very common but we have tried to build a simple interface:
trading_ws_*
to open websocket connections and query market data, look up state of our orders and close connectionsWe have build a test algorithm that works in this way: every time new market data is received we put an offer and a bid improving, by the minimun amount, the first line on the order book. By doing this, we try to stay on top of the order book every time.
This is a simple yey powerfull example. We have called it ‘The Molesto’.
Listing to market data is pretty much the same as what we have done within the ploting example, so I want to focus on the algorithm itself.
observe({
req(!is.null(global_algoritmos_1$data))
req(global_algoritmos_1$data())
logger_algoritmos_1 <- create.logger(logfile = glue("logs/the_molesto_{input$algoritmos_1_producto}_{Sys.Date()}.log"), level = "INFO")
# Objectivo: colocar una punta molesta, en el bid o en el ask cada vez que cambia el size en cualquiera de los casos
isolate({
market_data <- global_algoritmos_1$data()
productos <- global_productos()
global_algoritmos_1_ordenes$MinTradeVol <- productos %>% filter(Symbol == input$algoritmos_1_producto) %>% pull(MinTradeVol)
global_algoritmos_1_ordenes$MinPriceIncrement <- productos %>% filter(Symbol == input$algoritmos_1_producto) %>% pull(MinPriceIncrement)
if (some(.x = list("BI_price", "BI_size"), .p = ~ . %in% unlist(strsplit(market_data$Changes, ","))) &&
every(.x = list("BI_price", "BI_size"), .p = ~ . %in% colnames(market_data)) &&
!is.na(market_data$BI_price)) {
info(logger_algoritmos_1, str_c("- [BID] Cambia la punta compradora"))
if (is.null(global_algoritmos_1_ordenes$Bid)) {
bid <- trading_new_order(connection = global_connection$conn,
account = input$algoritmos_1_cuenta,
symbol = input$algoritmos_1_producto,
side = "Buy",
quantity = global_algoritmos_1_ordenes$MinTradeVol,
price = market_data$BI_price + global_algoritmos_1_ordenes$MinPriceIncrement)
global_algoritmos_1_ordenes$Bid <- append(x = global_algoritmos_1_ordenes$Bid, values = bid$clOrdId)
global_algoritmos_1_ordenes$BidPrice <- append(x = global_algoritmos_1_ordenes$BidPrice, values = bid$price)
info(logger_algoritmos_1, glue("- [BID] Primer compra - {price} - {status}",
price = bid$price,
status = bid$status))
} else {
if (market_data$BI_price == last(global_algoritmos_1_ordenes$BidPrice) &&
market_data$BI_size == global_algoritmos_1_ordenes$MinTradeVol) {
info(logger_algoritmos_1, glue("- [BID] No juego (soy yo)"))
} else {
bid <- trading_new_order(connection = global_connection$conn,
account = input$algoritmos_1_cuenta,
symbol = input$algoritmos_1_producto,
side = "Buy",
quantity = global_algoritmos_1_ordenes$MinTradeVol,
price = market_data$BI_price + global_algoritmos_1_ordenes$MinPriceIncrement)
global_algoritmos_1_ordenes$Bid <- append(x = global_algoritmos_1_ordenes$Bid, values = bid$clOrdId)
global_algoritmos_1_ordenes$BidPrice <- append(x = global_algoritmos_1_ordenes$BidPrice, values = bid$price)
trading_cancel_order(connection = global_connection$conn, id = nth(global_algoritmos_1_ordenes$Bid, -2), proprietary = "PBCP")
info(logger_algoritmos_1, glue("- [BID] Molesto - {price} - {status}",
price = bid$price,
status = bid$status))
}
}
}
})
})
We are only looking at the bid side just because the offer side works the same.
We can see that mostly everything has been wrapped up into an isolate function, this avoids to make build an infinite loop every time some new data entered into the algorithm. This is the most important thing to remember, everything else are just simple ‘if and else’ with the logic of what we have to do every time new data has been presented.
Anyone can test this algorithm by putting their own credentials from the test environment reMarkets. You can generate them here.
This is how it looks when the algorithm starts:
Again, this was not suppose to be an indepth guide on how to build a Shiny app, nor how to code algortithms for trading. Instead, this was only an atempt to show some use cases of the rRofex library within Shiny and to encourage those who want to use R on the financial markets. It is my hope that anyone that have seen this application could get some inspiration while building their own path.
Let’s stay in touch!