R Shiny Trading Dashboard for Kenyan Quant Traders
R Shiny production, Rhino framework, quantitative trading dashboard, forex strategy monitoring, enterprise data science
Your Backtested Strategy Looked Great. Then Live Trading Happened.
Most Kenyan retail forex traders running systematic strategies on MetaTrader 5 discover the same problem at the worst possible time: the backtest looked clean, but three weeks of live execution reveal a drawdown their spreadsheet never surfaced. The monitoring gap is not a trading problem. It is an engineering problem, and R/Shiny solves it for a fraction of institutional infrastructure costs.
Kenya’s retail forex community is one of the most active in Sub-Saharan Africa. The Capital Markets Authority estimates that retail forex trading volumes have grown significantly since the 2017 amendment that tightened broker regulation. Traders here are sophisticated: many run MT5 Expert Advisors, export trade histories to CSV, and build their own Excel models. But Excel is not a monitoring system. It does not stream data, flag regime changes, or compute drawdown metrics in real time. A Rhino-structured Shiny application does all three, and you can have the first version running in a weekend.
What Rhino Adds to a Production Shiny App
Rhino is an application framework developed by Appsilon for building Shiny apps with engineering-grade structure. Without it, Shiny projects tend to accumulate logic in server.R until the file becomes impossible to test or maintain. Rhino enforces a module system, separates UI from logic, and integrates testthat from the start.
A Rhino project for a trading dashboard has this directory layout:
trading-dashboard/
├── app/
│ ├── main.R # Entry point
│ ├── logic/
│ │ ├── trade_log.R # Data ingestion and cleaning
│ │ ├── drawdown.R # Drawdown computation
│ │ └── regime.R # Regime detection logic
│ └── view/
│ ├── overview.R # Overview tab UI + server
│ ├── drawdown.R # Drawdown tab UI + server
│ └── regime.R # Regime tab UI + server
├── tests/
│ └── testthat/
│ └── test-drawdown.R # Unit tests for risk metrics
├── renv.lock
└── rhino.yml
The logic/ layer is pure R: no Shiny dependencies, fully testable. The view/ layer contains Shiny modules that consume the logic layer’s output. This separation is what makes the dashboard maintainable once it reaches production.
Ingesting MT5 Trade Logs
MT5 exports a structured HTML or CSV report from the terminal’s “Account History” section. The CSV variant is easier to parse programmatically. A typical MT5 export has columns for ticket number, open time, close time, symbol, type, lots, open price, close price, and profit.
The ingestion module lives in app/logic/trade_log.R:
box::use(
dplyr[...],
readr[read_csv, cols, col_character, col_double, col_datetime],
lubridate[ymd_hms, with_tz]
)
#' Load and clean an MT5 trade history CSV
#'
#' @param path Path to the exported CSV file
#' @param tz Timezone of the MT5 server (e.g. "Africa/Nairobi")
#' @return A tibble with one row per closed trade
load_mt5_trades <- function(path, tz = "Africa/Nairobi") {
raw <- read_csv(path, col_types = cols(
Ticket = col_double(),
OpenTime = col_character(),
CloseTime = col_character(),
Symbol = col_character(),
Type = col_character(),
Lots = col_double(),
OpenPrice = col_double(),
ClosePrice = col_double(),
Profit = col_double()
))
raw |>
filter(Type %in% c("buy", "sell")) |>
mutate(
open_time = ymd_hms(OpenTime) |> with_tz(tz),
close_time = ymd_hms(CloseTime) |> with_tz(tz),
duration_hrs = as.numeric(close_time - open_time, units = "hours")
) |>
select(ticket = Ticket, symbol = Symbol, type = Type, lots = Lots,
open_price = OpenPrice, close_price = ClosePrice,
profit = Profit, open_time, close_time, duration_hrs) |>
arrange(close_time)
}The box::use() call at the top is Rhino’s preferred import style: explicit, testable, and collision-free. Setting the timezone to "Africa/Nairobi" (EAT, UTC+3) matters for any analysis involving trading sessions, because session-based performance metrics behave differently when timestamps are in the broker’s server time versus your local time.
The Drawdown Module
Drawdown is where most retail monitoring tools fall short. A broker dashboard shows open equity, but it does not show you the maximum drawdown from peak over your last 200 trades, the time spent in drawdown, or whether the current drawdown is within historical norms for your system.
The computation lives in app/logic/drawdown.R:
box::use(dplyr[...], tibble[tibble])
#' Compute running drawdown metrics from a trade sequence
#'
#' @param trades A tibble from load_mt5_trades()
#' @param initial_balance Starting account balance in USD
#' @return A tibble with equity curve and drawdown columns
compute_drawdown <- function(trades, initial_balance = 10000) {
trades |>
arrange(close_time) |>
mutate(
cumulative_pnl = cumsum(profit),
equity = initial_balance + cumulative_pnl,
running_peak = cummax(equity),
drawdown_abs = running_peak - equity,
drawdown_pct = drawdown_abs / running_peak
)
}
#' Summarise key drawdown statistics
#'
#' @param dd_tbl Output from compute_drawdown()
#' @return A named list of summary statistics
drawdown_summary <- function(dd_tbl) {
list(
max_drawdown_pct = max(dd_tbl$drawdown_pct),
max_drawdown_abs = max(dd_tbl$drawdown_abs),
current_dd_pct = tail(dd_tbl$drawdown_pct, 1),
trades_in_dd = sum(dd_tbl$drawdown_pct > 0),
recovery_factor = tail(dd_tbl$cumulative_pnl, 1) / max(dd_tbl$drawdown_abs)
)
}The recovery factor, total net profit divided by maximum drawdown, gives a single number that captures whether the system earns enough to justify the pain of its worst losing streak. A recovery factor below 1 means the system has not yet earned back its worst drawdown in total profits. Most retail traders do not track this number at all.
The Shiny module that renders this in the dashboard (app/view/drawdown.R) uses echarts4r for the equity curve chart, which renders cleanly on mobile browsers, a practical requirement for traders monitoring positions from a phone:
box::use(shiny[...], echarts4r[...])
box::use(app/logic/drawdown[compute_drawdown, drawdown_summary])
drawdown_ui <- function(id) {
ns <- NS(id)
tagList(
echarts4rOutput(ns("equity_curve")),
verbatimTextOutput(ns("dd_stats"))
)
}
drawdown_server <- function(id, trades, initial_balance) {
moduleServer(id, function(input, output, session) {
dd_data <- reactive({
compute_drawdown(trades(), initial_balance())
})
output$equity_curve <- renderEcharts4r({
dd_data() |>
e_charts(close_time) |>
e_line(equity, name = "Equity") |>
e_line(running_peak, name = "Peak", lineStyle = list(type = "dashed")) |>
e_area(drawdown_abs, name = "Drawdown") |>
e_tooltip(trigger = "axis") |>
e_theme("walden")
})
output$dd_stats <- renderPrint({
drawdown_summary(dd_data())
})
})
}Detecting Regime Shifts
A strategy optimised on 2022-2023 EURUSD data may stop working when the Fed’s rate cycle shifts. Monitoring for regime change is the difference between a system that adapts and one that silently bleeds an account. A simple but effective detector uses a rolling Sharpe Ratio with a rolling window and flags when it crosses a threshold.
The regime logic in app/logic/regime.R:
box::use(dplyr[...], slider[slide_dbl], zoo[rollmean])
#' Compute rolling Sharpe Ratio and flag regime shifts
#'
#' @param trades A tibble from load_mt5_trades()
#' @param window Rolling window in trades (default 50)
#' @param sr_warn Sharpe threshold below which a warning is flagged
#' @return A tibble with rolling Sharpe and a regime flag column
detect_regime <- function(trades, window = 50, sr_warn = 0.5) {
annualisation <- sqrt(252) # Assumes roughly one trade per day on average
trades |>
arrange(close_time) |>
mutate(
rolling_mean_pnl = slide_dbl(profit, mean, .before = window - 1,
.complete = TRUE),
rolling_sd_pnl = slide_dbl(profit, sd, .before = window - 1,
.complete = TRUE),
rolling_sr = (rolling_mean_pnl / rolling_sd_pnl) * annualisation,
regime_warning = !is.na(rolling_sr) & rolling_sr < sr_warn
)
}The slider package is well-suited here because it handles partial windows gracefully and integrates with the tidyverse pipe. When regime_warning flips to TRUE, the dashboard surfaces a red indicator in the overview tab. The threshold of 0.5 is conservative, consistent with the Deflated Sharpe Ratio reasoning that a Sharpe below a certain adjusted threshold signals the edge has degraded.
Deploying Without an Institutional Budget
The full Rhino app runs as a Docker container. A minimal Dockerfile for this dashboard:
FROM rocker/shiny-verse:4.4.0
RUN apt-get update && apt-get install -y libssl-dev libcurl4-openssl-dev
WORKDIR /app
COPY renv.lock renv.lock
RUN R -e "renv::restore(lockfile = 'renv.lock', prompt = FALSE)"
COPY . .
EXPOSE 3838
CMD ["R", "-e", "rhino::app() |> shiny::runApp(port = 3838, host = '0.0.0.0')"]A DigitalOcean Droplet at $12/month (2 vCPU, 2 GB RAM) handles this comfortably for a single user or a small team monitoring shared strategies. Add Nginx in front for SSL termination and the total infrastructure cost stays well under $20/month, comparable to a single Bloomberg terminal keyboard and far below the cost of a missed drawdown.
For teams already running MT5 on a VPS, co-locating the dashboard on the same machine eliminates latency in trade log access. The load_mt5_trades() function accepts any file path, so it reads directly from a shared network folder or a scheduled rsync from the MT5 server. For a deeper look at hosting options for Shiny applications, see the R Shiny hosting guide.
Where the Real Edge Goes
The traders who get consistent results from systematic strategies are not the ones with the best entry signals. They are the ones who know, in real time, whether their system is behaving within historical norms or has entered a new regime that their backtest never saw.
A Rhino-structured Shiny dashboard built the way described here gives a Kenyan quant trader the same observability infrastructure that quantitative hedge funds maintain at institutional scale: drawdown tracking, regime monitoring, and historical context, for the cost of a weekend’s work and a $12 VPS. The technology gap between a retail trader in Westlands and a prop desk in Sandton is far smaller than the monitoring gap.
The question worth sitting with: if your live performance diverged from your backtest six weeks ago, would your current tools tell you?
Building systematic forex monitoring infrastructure? Kwiz Computing Technologies builds production R/Shiny applications for trading teams across East Africa. See how we structure systematic trading in R and how the deflated Sharpe ratio can sharpen your strategy validation before you touch live capital.