Tutorial for Developing an Advanced Stock Dashboard for the S&P 500 for the 2025 Posit Table Competition | R bloggers

Tutorial for Developing an Advanced Stock Dashboard for the S&P 500 for the 2025 Posit Table Competition | R bloggers

5 minutes, 12 seconds Read

[This article was first published on Ozancan Ozdemir, and kindly contributed to R-bloggers]. (You can report a problem with the content on this page here)


Want to share your content on R bloggers? click here if you have a blog, or here if you don’t.

This tutorial describes the development of an R Shiny application titled S&P 500 monitoring dashboard for the Posit Table dashboard 2025. This app effectively combines interactive financial data visualization (plotly), beautiful data tables (gt, gtExtras), web scraping (rvest), and external API integration (riingo, ellmer/Gemini AI) within a custom, sleek dark theme. You can access the app this way link

1. Project overview and key technologies

This application is divided into several functional panels:

  1. Market overview: Top 10 S&P 500 companies by market cap, presented in a gt sparkline trends table.
  2. Inventory information: Interactive selection, real-time price headline, key statistics and a daily/comparative price chart (plotly).
  3. Fundamental analysis: Sortable table with key fundamental metrics.
  4. Market news and AI: News headlines (riingo) with sentiment analysis (sentimentr) and a large language model assistant (ellmer/Gemini AI).
  5. Portfolio calculator: Simple portfolio backtesting and metric calculation.

📚 Core R packages used

CategoryPackagesGoal
App and user interfaceshiny, htmltools, shinyLPApplication framework, custom HTML/CSS theme.
Get dataquantmod, rvest, xml2, riingoStock data retrieval (Yahoo), web scraping (S\&P 500, fundamentals) and news (Tiingo).
Data display/tablesgt, gtExtras, plotly, svgliteCreate highly stylish, professional data tables and interactive charts.
AI integrationellmer, shinychatConnect to the Gemini API for the financial assistant.
Utilitiesdplyr, stringr, lubridate, zooData cleaning, manipulation and processing of time series.

2. Setting up the environment and helper functions

The application starts by loading all necessary libraries and defining utility functions.

2.1. Icon and theme settings

The ICON_MAP list defines custom font-awesome icons used in the stats boxes, which sets the inline CSS style for specific colors.

2.2. Web scraping: get_sp500_tickers()

This crucial helper feature scrubs the S&P 500 ticker list from Wikipedia.

  • It uses rvest::read_html() And rvest::html_table() to extract the data.
  • It includes robust error handling (tryCatch) and a hardcoded fallback list if scraping fails.
  • Logo generation: It uses a Google Favicon service URL (https://www.google.com/s2/favicons?sz=64&domain=...) to dynamically generate company logos based on their domain, improving the visual appeal of the selection inputs and tables.

2.3. Get data and sparklines

  • pf_get_prices_for_sparkline(): Usage quantmod::getSymbols(src = "yahoo") to retrieve the last 30 days’ closing prices for a list of tickers, saving the data as a list of numeric vectors for use in gt sparkling lines.
  • spark_area_svg(): Uses this complex function ggplot2 And svglite::stringSVG() to generate a data URI that contains the SVG code for a colored area sparkline. This is essential for displaying the sparklines directly in it gt table cells without external image hosting.

3. The custom dark UI (ui.R)

The premium look of the dashboard is achieved entirely through custom CSS within the tags$head part of the ui.

3.1. Custom CSS theme

The CSS (embedded with tags$style(HTML(...))):

  • Sets a dark blue/black linear gradient background for the body.
  • Uses modern fonts such as ‘Under’ And ‘JetBrains Mono’ (a monospace font often used for finance/coding) for a clean, technical look.
  • Applies different background colors (#131722, #1e2431, #252b3d) and border colors (#2a2e39) to different elements (.main-container, .stock-header, .stat-card, .selectize-input) to create a layered, visually separated design.
  • Defines different colors for price changes: Vegetable (#26a69a) for op And Red (#ef5350) for down.
  • Adjusts scroll bars, input (selectize-input), and the fixed app-footer for consistency.

3.2. Layout structure

The fluidPage uses a simple three column layout within the main-container (which is a custom made one div):

  • Top half (left): Market capitalization table, fundamental statistics table.
  • Top half (right): Stock Picker, Custom Header (uiOutput("top_header_ui")), Key Stats Cards, Pricing Tables (gt_output), and the main chart (plotlyOutput).
  • Bottom half: Riingo news (gt_output) and AI chat (chat_mod_ui).
  • Bottom global: Portfolio calculator panel.

4. The server logic (server.R)

The server manages data retrieval, reactive computation, and display of all output.

4.1. Stock data of reactive substances

  • stock_data1 And stock_data2: This reactive calling objects get_stock_data() (which uses quantmod::getSymbols) to retrieve stock quotes based on the selected ticker(s) and time interval. They are caused by changes in ticker1, intervalor the manual REFRESH DATA knob (input$refresh).
  • output$top_header_ui: This renderUI function dynamically generates the HTML header, including the company logo, name and formatted last price and daily change.
    • make_stock_header() retrieves the logo and name.
    • make_price_bar() calculates the last price and the daily percentage change, where a .price-up (green) or .price-down (red) CSS class for visual feedback.
  • The statistical renderText outputs (Volume, 52W High/Low, Avg. Volume) use the stock_data1() reactive.

4.3. Advanced GT table view

The dashboard uses three main ones gt tables, each with heavy custom work:

A. Key Statistics Table (output$key_stats_table)

This table shows market capitalization, revenue and a 30-day price trend for the top stocks.

  • Data preparation: The data comes from get_market_cap_data()that scraps a stock analysis website using rvest.
  • Logo/name formatting: The Name_Logo column is transformed using gt::text_transform to embed the logo image ( tag) next to the company name, thanks to the custom HTML/CSS used.
  • Visualization of market capitalization: gtExtras::gt_plt_bar() creates a simple bar chart in the cell to display relative market capitalization.
  • Visualization of earnings: gtExtras::gt_color_box() adds a background color box based on the sales value.
  • Sparkline trend: Used this gt::text_transform the habit of calling spark_area_svg() helper, embedding the generated SVG data URI as cell content.

B. Table with daily prices (output$price_table1)

This table shows the last 5 days of OHLC (Open, High, Low, Close) and the main technical indicators (RSI, MACD).

  • Indicator calculation: Inside create_table()technical analysis is carried out using quantmod::RSI() And quantmod::MACD().
  • Conditional formatting:
    • Change/Change %: Uses a complex text_transform to display green (▲) or red (▼) arrows and text color based on the price movement.
    • RSI: Usage gt::tab_style of gt::cells_body to highlight the cell background when RSI is bought over (>= 70) or oversold (<= 30).

C. Riingo News table (output$riingo_news_gt)

This table shows news headlines based on the selected ticker and source.

  • Get news: riingo_news_data() used riingo::riingo_news() (requires a Tiingo API token, which is hardcoded in this example: TIINGO_TOKEN <- "8c7094ec74e7fc1ceca99a468fc4770df03dd0ec")
  • Sentiment analysis: sentimentr::sentiment_by() is used to quickly classify the sentiment in the headline as ‘Positive’, ‘Negative’ or ‘Neutral’.
  • Visualization:
    • Source logo: A text_transform embed the news source’s logo ( label).
    • Sentiment highlight: gt::tab_style Conditionally colors the background and text of the Sentiment column based on the calculated label (green/red/gray).

4.4. Interactive plotting (output$price_plot)

  • Candlestick chart (no comparison): When input$compare_mode is FALSEthe code used plotly::plot_ly(type = "candlestick") to display the OHLC data, by setting custom increasing/decreasing colors (#26a69a / #ef5350).
  • Normalized comparison (comparison mode): When TRUEit calculates the percentage change from day one for both selected stocks and applications plotly::add_lines() to plot their performance curves on the same normalized Y-axis, which is standard practice for performance comparison.

4.5. Portfolio backtesting

The Portfolio calculator section implements classic financial backtesting logic:

  • pf_get_prices(): Downloads the closing prices for the selected tickers.
  • pf_returns(): Calculates daily returns ($R_t = \frac{P_t}{P_{t-1}} – 1$).
  • pf_port_ret(): Calculates the daily return of the portfolio ($\sum w_i R_{i,t}$) based on the user-entered weights (pf_weights()).
  • pf_equity(): Calculates the cumulative value of the portfolio over time based on the starting capital ($Equity_t = Capital \times \prod (1 + R_{port,t})$).
  • Statistics: Calculates key performance indicators:
    • CAGR (compound annual growth rate): Annual return.
    • Inconstancy: Annualized standard deviation of returns.
    • Sharp ratio: Measures return per unit of risk ($\frac{Annualized Return – Risk-Free Rate}{Annualized Volatility}$).
  • output$pf_alloc_table: Displays the final assignment using the custom weight_pill_html() feature, which creates a sleek, dynamically filled progress bar for the weight percentage within the gt table.

4.6. AI Chat integration

  • The code sets a reactive chat_client using the ellmer::chat_google_gemini() function from the ellmer package.
  • Users must use their Gemini API key and click SET API KEY to initialize the client.
  • The system_prompt is used to instruct the AI ​​to act as an ‘expert financial advisor and stock analyst’, ensuring relevant responses.
  • chat_mod_server("stock_chat", chat_client()) connects the initialized AI client to the shinychat UI module, which makes the chat functionality live.


#Tutorial #Developing #Advanced #Stock #Dashboard #Posit #Table #Competition #bloggers

Similar Posts

Leave a Reply

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