# Load dimension table
etf_master <- readRDS(file.path(STAR_SCHEMA_DIR, "etf_master.rds"))

# Open fact table (Parquet dataset)
etf_daily <- open_dataset(file.path(STAR_SCHEMA_DIR, "etf_daily"))

# Calculate comprehensive volume statistics for each ETF
volume_stats <- etf_daily %>%
  filter(trade_date >= START_DATE & trade_date <= END_DATE) %>%
  filter(!is.na(volume_cleaned)) %>%
  collect() %>%
  group_by(stock_code) %>%
  summarise(
    total_days = n(),
    days_with_volume = sum(volume_cleaned > 0, na.rm = TRUE),
    days_with_zero_volume = sum(volume_cleaned == 0, na.rm = TRUE),
    days_with_na = sum(is.na(volume_cleaned)),
    total_volume = sum(volume_cleaned, na.rm = TRUE),
    avg_volume = mean(volume_cleaned, na.rm = TRUE),
    median_volume = median(volume_cleaned, na.rm = TRUE),
    max_volume = max(volume_cleaned, na.rm = TRUE),
    min_volume = min(volume_cleaned, na.rm = TRUE),
    trading_frequency = days_with_volume / total_days,
    avg_volume_when_traded = mean(volume_cleaned[volume_cleaned > 0], na.rm = TRUE),
    volume_cv = ifelse(avg_volume > 0, 
                       sd(volume_cleaned, na.rm = TRUE) / avg_volume, 
                       NA),
    p25_volume = quantile(volume_cleaned, 0.25, na.rm = TRUE),
    p75_volume = quantile(volume_cleaned, 0.75, na.rm = TRUE),
    p90_volume = quantile(volume_cleaned, 0.90, na.rm = TRUE),
    p95_volume = quantile(volume_cleaned, 0.95, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(
    date_range_start = START_DATE,
    date_range_end = END_DATE,
    date_range_days = as.numeric(END_DATE - START_DATE) + 1
  )

# Calculate breaks for categorization
adv_breaks <- quantile(volume_stats$avg_volume, 
                       probs = c(0, 0.1, 0.25, 0.5, 0.75, 0.9, 1), 
                       na.rm = TRUE)

total_vol_breaks <- quantile(volume_stats$total_volume, 
                             probs = c(0, 0.1, 0.25, 0.5, 0.75, 0.9, 1), 
                             na.rm = TRUE)

# Categorize ETFs
volume_stats <- volume_stats %>%
  mutate(
    volume_category_adv = case_when(
      avg_volume == 0 | is.na(avg_volume) ~ "Zero Volume",
      avg_volume < adv_breaks[2] ~ "Very Low Volume (Bottom 10%)",
      avg_volume < adv_breaks[3] ~ "Low Volume (10-25%)",
      avg_volume < adv_breaks[4] ~ "Below Average Volume (25-50%)",
      avg_volume < adv_breaks[5] ~ "Above Average Volume (50-75%)",
      avg_volume < adv_breaks[6] ~ "High Volume (75-90%)",
      TRUE ~ "Very High Volume (Top 10%)"
    ),
    volume_category_frequency = case_when(
      trading_frequency == 0 ~ "Never Traded",
      trading_frequency < 0.1 ~ "Rarely Traded (<10% of days)",
      trading_frequency < 0.25 ~ "Infrequently Traded (10-25% of days)",
      trading_frequency < 0.5 ~ "Occasionally Traded (25-50% of days)",
      trading_frequency < 0.75 ~ "Regularly Traded (50-75% of days)",
      trading_frequency < 0.95 ~ "Frequently Traded (75-95% of days)",
      TRUE ~ "Very Frequently Traded (>95% of days)"
    ),
    volume_category_combined = case_when(
      avg_volume == 0 | trading_frequency == 0 ~ "Inactive (No Trading)",
      avg_volume < adv_breaks[2] & trading_frequency < 0.25 ~ "Very Low Activity",
      avg_volume < adv_breaks[4] & trading_frequency < 0.5 ~ "Low Activity",
      avg_volume < adv_breaks[5] & trading_frequency < 0.75 ~ "Moderate Activity",
      avg_volume >= adv_breaks[5] & trading_frequency >= 0.75 ~ "High Activity",
      avg_volume >= adv_breaks[6] & trading_frequency >= 0.9 ~ "Very High Activity",
      TRUE ~ "Mixed Activity"
    ),
    volume_category_total = case_when(
      total_volume == 0 | is.na(total_volume) ~ "Zero Total Volume",
      total_volume < total_vol_breaks[2] ~ "Very Low Total Volume (Bottom 10%)",
      total_volume < total_vol_breaks[3] ~ "Low Total Volume (10-25%)",
      total_volume < total_vol_breaks[4] ~ "Below Average Total Volume (25-50%)",
      total_volume < total_vol_breaks[5] ~ "Above Average Total Volume (50-75%)",
      total_volume < total_vol_breaks[6] ~ "High Total Volume (75-90%)",
      TRUE ~ "Very High Total Volume (Top 10%)"
    )
  )

# Join with master data
volume_categorized <- volume_stats %>%
  left_join(etf_master, by = "stock_code") %>%
  select(
    stock_code,
    stock_short_name,
    issuer,
    asset_class,
    geographic_focus,
    investment_focus,
    listing_date,
    total_volume,
    avg_volume,
    median_volume,
    max_volume,
    trading_frequency,
    days_with_volume,
    total_days,
    volume_category_adv,
    volume_category_frequency,
    volume_category_combined,
    volume_category_total,
    volume_cv,
    avg_volume_when_traded
  ) %>%
  arrange(desc(avg_volume))

# Calculate summary statistics
overall_stats <- volume_categorized %>%
  summarise(
    total_etfs = n(),
    etfs_with_zero_volume = sum(avg_volume == 0 | is.na(avg_volume)),
    etfs_with_some_volume = sum(avg_volume > 0, na.rm = TRUE),
    median_avg_volume = median(avg_volume, na.rm = TRUE),
    mean_avg_volume = mean(avg_volume, na.rm = TRUE),
    median_trading_frequency = median(trading_frequency, na.rm = TRUE),
    mean_trading_frequency = mean(trading_frequency, na.rm = TRUE),
    total_volume_all_etfs = sum(total_volume, na.rm = TRUE)
  )

Data Overview

cat("**Date Range:**", format(START_DATE), "to", format(END_DATE), "\n\n")
## **Date Range:** 2025-09-15 to 2025-12-17
cat("**Total ETFs Analyzed:**", nrow(volume_categorized), "\n\n")
## **Total ETFs Analyzed:** 354
cat("**ETFs with Zero Volume:**", overall_stats$etfs_with_zero_volume, "\n\n")
## **ETFs with Zero Volume:** 0
cat("**ETFs with Trading Activity:**", overall_stats$etfs_with_some_volume, "\n\n")
## **ETFs with Trading Activity:** 354
cat("**Analysis Date:**", format(Sys.Date()))
## **Analysis Date:** 2025-12-18

Volume Distribution Analysis

Plot 1: Volume Distribution Histogram (Linear Scale)

This histogram shows the distribution of average daily volume across all ETFs using a linear scale on the x-axis. The linear scale shows actual volume values directly, making it easy to read specific numbers.

Limitation of Linear Scale:

When volume values span many orders of magnitude (from very low to very high), a linear scale has a significant limitation: most ETFs cluster at the lower end of the scale, and the high-volume ETFs (even if they represent 10% of all ETFs) get compressed into a tiny space on the far right. This makes it difficult to see the distribution of high-volume ETFs clearly. The histogram may appear to show most ETFs at low volumes, but this is partly an artifact of the scale - the high-volume ETFs are there, just not visible because they occupy a very small portion of the x-axis range.

# Filter out zero volume for better visualization
volume_for_plot <- volume_categorized %>%
  filter(avg_volume > 0)

# Calculate some statistics for context
max_vol <- max(volume_for_plot$avg_volume, na.rm = TRUE)
p90_vol <- quantile(volume_for_plot$avg_volume, 0.90, na.rm = TRUE)
p95_vol <- quantile(volume_for_plot$avg_volume, 0.95, na.rm = TRUE)

ggplot(volume_for_plot, aes(x = avg_volume)) +
  geom_histogram(bins = 50, fill = "#007bff", alpha = 0.7, color = "white") +
  labs(
    title = "Distribution of Average Daily Volume (Linear Scale)",
    subtitle = paste("ETFs with trading activity:", nrow(volume_for_plot), 
                     "| Max volume:", formatC(max_vol, format = "d", big.mark = ","),
                     "| 90th percentile:", formatC(p90_vol, format = "d", big.mark = ",")),
    x = "Average Daily Volume",
    y = "Number of ETFs",
    caption = "Note: High-volume ETFs may appear compressed on the right side due to wide range of values"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 14, face = "bold"),
    plot.subtitle = element_text(size = 10, color = "gray50"),
    axis.title = element_text(size = 11),
    axis.text = element_text(size = 9),
    plot.caption = element_text(size = 9, color = "gray60", hjust = 0),
    panel.grid.minor = element_blank()
  ) +
  scale_x_continuous(labels = scales::number_format(accuracy = 1, big.mark = ",")) +
  scale_y_continuous(labels = scales::number_format(accuracy = 1))

Plot 1b: Volume Distribution Histogram (Log Scale)

This histogram shows the same distribution but uses a logarithmic scale on the x-axis. The log scale compresses the wide range of volume values, allowing us to see the distribution pattern more clearly across all volume levels, including the high-volume ETFs.

Understanding the Log Scale:

The logarithmic scale transforms volume values so that equal distances on the x-axis represent equal ratios rather than equal differences. For example: - On a log scale, the distance between 1,000 and 10,000 is the same as between 10,000 and 100,000 - This means a 10x increase in volume appears as the same distance regardless of the starting point

How to Read Actual Volume from Log Scale:

To understand the actual volume values on a log scale: - 1e+03 = 1,000 shares - 1e+04 = 10,000 shares
- 1e+05 = 100,000 shares - 1e+06 = 1,000,000 shares - 1e+07 = 10,000,000 shares

The histogram reveals whether ETFs are evenly distributed across volume ranges or if there’s concentration at certain levels. A normal-looking distribution on a log scale suggests that ETF volumes follow a log-normal distribution, which is common in financial data. This scale makes it much easier to see the high-volume ETFs that may be hidden in the linear scale version above.

# Filter out zero volume for better visualization
vol_data_plot <- volume_categorized %>%
  filter(avg_volume > 0) %>%
  mutate(log_avg_volume = log10(avg_volume + 1))

ggplot(vol_data_plot, aes(x = log_avg_volume)) +
  geom_histogram(bins = 50, fill = "steelblue", alpha = 0.7, color = "white") +
  labs(
    title = "Distribution of Average Daily Volume (Log Scale)",
    subtitle = paste("Date Range:", format(START_DATE), "to", format(END_DATE)),
    x = "Log10(Average Daily Volume)",
    y = "Number of ETFs",
    caption = "Log scale reveals distribution across all volume levels, including high-volume ETFs"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 14, face = "bold"),
    plot.subtitle = element_text(size = 10, color = "gray50"),
    axis.title = element_text(size = 11),
    axis.text = element_text(size = 9),
    plot.caption = element_text(size = 9, color = "gray60", hjust = 0)
  )


Category Distribution

Plot 2: Distribution by Average Daily Volume Category

This chart shows how ETFs are distributed across volume categories. Each bar represents the number of ETFs in that category, with percentages shown on top. The color gradient (red to green) helps visualize the progression from low to high volume categories.

category_counts <- volume_categorized %>%
  count(volume_category_adv) %>%
  mutate(
    pct = round(100 * n / sum(n), 1),
    volume_category_adv = factor(volume_category_adv,
      levels = c("Zero Volume",
                "Very Low Volume (Bottom 10%)",
                "Low Volume (10-25%)",
                "Below Average Volume (25-50%)",
                "Above Average Volume (50-75%)",
                "High Volume (75-90%)",
                "Very High Volume (Top 10%)"))
  )

ggplot(category_counts, aes(x = volume_category_adv, y = n, fill = volume_category_adv)) +
  geom_bar(stat = "identity", alpha = 0.8) +
  geom_text(aes(label = paste0(n, "\n(", pct, "%)")), 
            vjust = -0.3, size = 3) +
  scale_fill_brewer(palette = "RdYlGn", direction = -1) +
  labs(
    title = "ETF Distribution by Average Daily Volume Category",
    x = "Volume Category",
    y = "Number of ETFs"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 14, face = "bold"),
    axis.text.x = element_text(angle = 45, hjust = 1, size = 9),
    legend.position = "none"
  )

Plot 3: Distribution by Trading Frequency Category

This chart shows how frequently ETFs trade. It reveals whether most ETFs trade regularly or if many have sporadic trading activity. The bars show both the count and percentage of ETFs in each frequency category.

freq_counts <- volume_categorized %>%
  count(volume_category_frequency) %>%
  mutate(
    pct = round(100 * n / sum(n), 1),
    volume_category_frequency = factor(volume_category_frequency,
      levels = c("Never Traded",
                "Rarely Traded (<10% of days)",
                "Infrequently Traded (10-25% of days)",
                "Occasionally Traded (25-50% of days)",
                "Regularly Traded (50-75% of days)",
                "Frequently Traded (75-95% of days)",
                "Very Frequently Traded (>95% of days)"))
  )

ggplot(freq_counts, aes(x = volume_category_frequency, y = n, fill = volume_category_frequency)) +
  geom_bar(stat = "identity", alpha = 0.8) +
  geom_text(aes(label = paste0(n, "\n(", pct, "%)")), 
            vjust = -0.3, size = 3) +
  scale_fill_brewer(palette = "Blues") +
  labs(
    title = "ETF Distribution by Trading Frequency",
    x = "Trading Frequency Category",
    y = "Number of ETFs"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 14, face = "bold"),
    axis.text.x = element_text(angle = 45, hjust = 1, size = 9),
    legend.position = "none"
  )


Volume vs Trading Frequency Analysis

Plot 4: Volume vs Trading Frequency Scatter Plot

This scatter plot shows the relationship between average daily volume (on log scale) and trading frequency for each ETF. Each point represents one ETF, colored by its combined activity category. This visualization helps identify patterns such as whether high-volume ETFs also trade frequently, or if there are ETFs with high volume but low trading frequency (indicating sporadic but large trades).

scatter_data <- volume_categorized %>%
  filter(avg_volume > 0) %>%
  mutate(
    log_avg_volume = log10(avg_volume + 1),
    volume_category_combined = factor(volume_category_combined,
      levels = c("Inactive (No Trading)",
                "Very Low Activity",
                "Low Activity",
                "Moderate Activity",
                "High Activity",
                "Very High Activity",
                "Mixed Activity"))
  )

ggplot(scatter_data, aes(x = trading_frequency, y = log_avg_volume, 
                         color = volume_category_combined)) +
  geom_point(alpha = 0.6, size = 2) +
  scale_color_brewer(palette = "RdYlGn", name = "Activity Category") +
  scale_x_continuous(labels = percent_format(), breaks = seq(0, 1, 0.2)) +
  labs(
    title = "Average Daily Volume vs Trading Frequency",
    subtitle = "Each point represents one ETF",
    x = "Trading Frequency (Proportion of Days with Trading)",
    y = "Log10(Average Daily Volume)"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 14, face = "bold"),
    plot.subtitle = element_text(size = 10, color = "gray50"),
    legend.position = "right"
  )


Combined Activity Analysis

Plot 5: Combined Activity Category Distribution

This chart shows the distribution of ETFs by their combined activity category, which considers both volume level and trading frequency together. This provides a more holistic view of ETF trading activity than looking at volume or frequency alone.

combined_counts <- volume_categorized %>%
  count(volume_category_combined) %>%
  mutate(
    pct = round(100 * n / sum(n), 1),
    volume_category_combined = factor(volume_category_combined,
      levels = c("Inactive (No Trading)",
                "Very Low Activity",
                "Low Activity",
                "Moderate Activity",
                "High Activity",
                "Very High Activity",
                "Mixed Activity"))
  )

ggplot(combined_counts, aes(x = volume_category_combined, y = n, fill = volume_category_combined)) +
  geom_bar(stat = "identity", alpha = 0.8) +
  geom_text(aes(label = paste0(n, "\n(", pct, "%)")), 
            vjust = -0.3, size = 3) +
  scale_fill_brewer(palette = "RdYlGn", direction = -1) +
  labs(
    title = "ETF Distribution by Combined Activity Category",
    subtitle = "Combines volume level and trading frequency",
    x = "Activity Category",
    y = "Number of ETFs"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 14, face = "bold"),
    plot.subtitle = element_text(size = 10, color = "gray50"),
    axis.text.x = element_text(angle = 45, hjust = 1, size = 9),
    legend.position = "none"
  )


Volume by Asset Class

Plot 6: Volume Distribution by Asset Class

This violin plot with boxplots shows how average daily volume (on log scale) varies across different asset classes. The violin shape shows the distribution density, while the boxplot shows quartiles and outliers. This helps identify which asset classes tend to have higher or lower trading volumes.

if ("asset_class" %in% colnames(volume_categorized)) {
  asset_class_data <- volume_categorized %>%
    filter(!is.na(asset_class) & avg_volume > 0) %>%
    mutate(log_avg_volume = log10(avg_volume + 1))
  
  # Get top asset classes by count
  top_asset_classes <- asset_class_data %>%
    count(asset_class, sort = TRUE) %>%
    head(10) %>%
    pull(asset_class)
  
  asset_class_data <- asset_class_data %>%
    filter(asset_class %in% top_asset_classes)
  
  ggplot(asset_class_data, aes(x = asset_class, y = log_avg_volume, fill = asset_class)) +
    geom_violin(alpha = 0.7) +
    geom_boxplot(width = 0.2, alpha = 0.5, outlier.alpha = 0.3) +
    scale_fill_brewer(palette = "Set3") +
    labs(
      title = "Volume Distribution by Asset Class",
      subtitle = "Top 10 asset classes by ETF count",
      x = "Asset Class",
      y = "Log10(Average Daily Volume)"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(size = 14, face = "bold"),
      plot.subtitle = element_text(size = 10, color = "gray50"),
      axis.text.x = element_text(angle = 45, hjust = 1, size = 9),
      legend.position = "none"
    )
}


Volume Concentration Analysis

Plot 7: Cumulative Volume Distribution (Pareto Chart)

This Pareto chart shows the concentration of trading volume across ETFs. The line shows what percentage of total volume is accounted for by the top X% of ETFs (ranked by volume). The dashed lines indicate the 80/20 rule: if the line crosses the 80% volume mark at the 20% ETF mark, it indicates that 20% of ETFs account for 80% of volume - a common pattern in financial markets known as the Pareto principle.

pareto_data <- volume_categorized %>%
  filter(avg_volume > 0) %>%
  arrange(desc(total_volume)) %>%
  mutate(
    cum_volume = cumsum(total_volume),
    cum_pct = 100 * cum_volume / sum(total_volume),
    rank = row_number(),
    cum_pct_etfs = 100 * rank / n()
  )

ggplot(pareto_data, aes(x = cum_pct_etfs)) +
  geom_line(aes(y = cum_pct), color = "steelblue", linewidth = 1.2) +
  geom_hline(yintercept = 80, linetype = "dashed", color = "red", alpha = 0.7) +
  geom_vline(xintercept = 20, linetype = "dashed", color = "red", alpha = 0.7) +
  annotate("text", x = 25, y = 85, label = "80% of volume", color = "red", size = 3) +
  annotate("text", x = 22, y = 10, label = "Top 20% of ETFs", color = "red", size = 3) +
  scale_x_continuous(limits = c(0, 100), breaks = seq(0, 100, 20)) +
  scale_y_continuous(limits = c(0, 100), breaks = seq(0, 100, 20)) +
  labs(
    title = "Pareto Chart: Volume Concentration",
    subtitle = "Cumulative percentage of total volume vs cumulative percentage of ETFs",
    x = "Cumulative % of ETFs (ranked by volume)",
    y = "Cumulative % of Total Volume"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 14, face = "bold"),
    plot.subtitle = element_text(size = 10, color = "gray50")
  )


Top ETFs

Plot 8: Top 20 ETFs by Average Daily Volume

This horizontal bar chart shows the top 20 ETFs ranked by average daily volume. The bars are color-coded by volume level, making it easy to identify the most actively traded ETFs. Volume is displayed in millions for readability.

top_20 <- volume_categorized %>%
  filter(avg_volume > 0) %>%
  arrange(desc(avg_volume)) %>%
  head(20) %>%
  mutate(
    stock_label = ifelse(!is.na(stock_short_name) & stock_short_name != "",
                       paste0(stock_code, "\n", stock_short_name),
                       stock_code),
    stock_label = factor(stock_label, levels = rev(stock_label))
  )

ggplot(top_20, aes(x = stock_label, y = avg_volume, fill = avg_volume)) +
  geom_bar(stat = "identity", alpha = 0.8) +
  scale_fill_gradient(low = "lightblue", high = "darkblue", name = "Avg Volume") +
  scale_y_continuous(labels = comma_format(scale = 1e-6, suffix = "M")) +
  coord_flip() +
  labs(
    title = "Top 20 ETFs by Average Daily Volume",
    x = "ETF",
    y = "Average Daily Volume (Millions)"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 14, face = "bold"),
    axis.text.y = element_text(size = 8),
    legend.position = "right"
  )


Summary Statistics

Overall Statistics

overall_stats %>%
  gt() %>%
  fmt_number(columns = c(median_avg_volume, mean_avg_volume, total_volume_all_etfs), 
             decimals = 0, use_seps = TRUE) %>%
  fmt_number(columns = c(median_trading_frequency, mean_trading_frequency), 
             decimals = 3) %>%
  cols_label(
    total_etfs = "Total ETFs",
    etfs_with_zero_volume = "ETFs with Zero Volume",
    etfs_with_some_volume = "ETFs with Trading Activity",
    median_avg_volume = "Median Avg Volume",
    mean_avg_volume = "Mean Avg Volume",
    median_trading_frequency = "Median Trading Frequency",
    mean_trading_frequency = "Mean Trading Frequency",
    total_volume_all_etfs = "Total Volume (All ETFs)"
  ) %>%
  tab_style(
    style = cell_text(weight = "bold"),
    locations = cells_column_labels()
  ) %>%
  tab_style(
    style = cell_text(align = "right"),
    locations = cells_body()
  ) %>%
  opt_table_font(font = "Arial") %>%
  tab_options(
    table.width = pct(80),
    column_labels.background.color = "#f8f9fa",
    table_body.hlines.color = "#e9ecef",
    heading.border.bottom.color = "#dee2e6"
  )
Total ETFs ETFs with Zero Volume ETFs with Trading Activity Median Avg Volume Mean Avg Volume Median Trading Frequency Mean Trading Frequency Total Volume (All ETFs)
354 0 354 12,897 9,231,415 1.000 1.000 205,116,148,576

Distribution by Average Daily Volume Category

summary_adv_detailed <- volume_categorized %>%
  group_by(volume_category_adv) %>%
  summarise(
    count = n(),
    pct_of_total = round(100 * n() / nrow(volume_categorized), 1),
    avg_avg_volume = round(mean(avg_volume, na.rm = TRUE), 2),
    median_avg_volume = round(median(avg_volume, na.rm = TRUE), 2),
    .groups = "drop"
  ) %>%
  arrange(desc(avg_avg_volume))

summary_adv_detailed %>%
  gt() %>%
  fmt_number(columns = c(avg_avg_volume, median_avg_volume), 
             decimals = 0, use_seps = TRUE) %>%
  fmt_number(columns = pct_of_total, decimals = 1, pattern = "{x}%") %>%
  cols_label(
    volume_category_adv = "Category",
    count = "Count",
    pct_of_total = "% of Total",
    avg_avg_volume = "Avg Volume",
    median_avg_volume = "Median Volume"
  ) %>%
  tab_style(
    style = cell_text(weight = "bold"),
    locations = cells_column_labels()
  ) %>%
  tab_style(
    style = cell_text(align = "right"),
    locations = cells_body()
  ) %>%
  opt_table_font(font = "Arial") %>%
  tab_options(
    table.width = pct(100),
    column_labels.background.color = "#f8f9fa",
    table_body.hlines.color = "#e9ecef",
    heading.border.bottom.color = "#dee2e6"
  )
Category Count % of Total Avg Volume Median Volume
Very High Volume (Top 10%) 36 10.2% 90,240,258 2,667,339
High Volume (75-90%) 53 15.0% 282,958 222,050
Above Average Volume (50-75%) 88 24.9% 41,003 33,100
Below Average Volume (25-50%) 88 24.9% 6,629 5,742
Low Volume (10-25%) 53 15.0% 1,468 1,435
Very Low Volume (Bottom 10%) 36 10.2% 150 121

Distribution by Trading Frequency Category

summary_freq_detailed <- volume_categorized %>%
  group_by(volume_category_frequency) %>%
  summarise(
    count = n(),
    pct_of_total = round(100 * n() / nrow(volume_categorized), 1),
    avg_frequency = round(mean(trading_frequency, na.rm = TRUE), 3),
    median_frequency = round(median(trading_frequency, na.rm = TRUE), 3),
    .groups = "drop"
  ) %>%
  arrange(desc(avg_frequency))

summary_freq_detailed %>%
  gt() %>%
  fmt_number(columns = c(avg_frequency, median_frequency), decimals = 3) %>%
  fmt_number(columns = pct_of_total, decimals = 1, pattern = "{x}%") %>%
  cols_label(
    volume_category_frequency = "Category",
    count = "Count",
    pct_of_total = "% of Total",
    avg_frequency = "Avg Frequency",
    median_frequency = "Median Frequency"
  ) %>%
  tab_style(
    style = cell_text(weight = "bold"),
    locations = cells_column_labels()
  ) %>%
  tab_style(
    style = cell_text(align = "right"),
    locations = cells_body()
  ) %>%
  opt_table_font(font = "Arial") %>%
  tab_options(
    table.width = pct(100),
    column_labels.background.color = "#f8f9fa",
    table_body.hlines.color = "#e9ecef",
    heading.border.bottom.color = "#dee2e6"
  )
Category Count % of Total Avg Frequency Median Frequency
Very Frequently Traded (>95% of days) 354 100.0% 1.000 1.000

Top and Bottom ETFs

Top 20 ETFs by Average Daily Volume

top_20 <- volume_categorized %>%
  head(20) %>%
  select(stock_code, stock_short_name, issuer, asset_class,
         avg_volume, total_volume, trading_frequency, volume_category_combined)

top_20 %>%
  gt() %>%
  fmt_number(columns = c(avg_volume, total_volume), 
             decimals = 0, use_seps = TRUE) %>%
  fmt_number(columns = trading_frequency, decimals = 3) %>%
  cols_label(
    stock_code = "Code",
    stock_short_name = "Name",
    issuer = "Issuer",
    asset_class = "Asset Class",
    avg_volume = "Avg Volume",
    total_volume = "Total Volume",
    trading_frequency = "Trading Frequency",
    volume_category_combined = "Activity Category"
  ) %>%
  tab_style(
    style = cell_text(weight = "bold"),
    locations = cells_column_labels()
  ) %>%
  tab_style(
    style = cell_text(align = "right"),
    locations = cells_body(columns = c(avg_volume, total_volume, trading_frequency))
  ) %>%
  opt_table_font(font = "Arial") %>%
  tab_options(
    table.width = pct(100),
    column_labels.background.color = "#f8f9fa",
    table_body.hlines.color = "#e9ecef",
    heading.border.bottom.color = "#dee2e6"
  )
Code Name Issuer Asset Class Avg Volume Total Volume Trading Frequency Activity Category
3033 CSOP HS TECH CSOP Asset Management Limited Equity 1,438,832,065 90,646,420,097 1.000 High Activity
7552 XI2CSOPHSTECH CSOP Asset Management Limited Equity 558,089,312 35,159,626,650 1.000 High Activity
2800 TRACKER FUND Hang Seng Investment Management Limited Equity 541,739,769 34,129,605,433 1.000 High Activity
7226 XL2CSOPHSTECH CSOP Asset Management Limited Equity 222,902,218 14,042,839,713 1.000 High Activity
7500 FI2 CSOP HSI CSOP Asset Management Limited Equity 172,591,598 10,873,270,704 1.000 High Activity
2828 HSCEI ETF Hang Seng Investment Management Limited Equity 85,423,320 5,381,669,175 1.000 High Activity
7200 FL2 CSOP HSI CSOP Asset Management Limited Equity 40,959,594 2,580,454,428 1.000 High Activity
3032 HSTECH ETF Hang Seng Investment Management Limited Equity 39,003,682 2,457,231,949 1.000 High Activity
3067 ISHARESHSTECH BlackRock Asset Management North Asia Limited Equity 35,304,585 2,224,188,855 1.000 High Activity
3416 A GX HSCEICC Mirae Asset Global Investments (Hong Kong) Limited Equity 28,130,622 1,772,229,208 1.000 High Activity
7709 XL2CSOPHYNIX CSOP Asset Management Limited Equity 22,522,740 991,000,565 1.000 High Activity
7568 FI2CSOPNASDAQ CSOP Asset Management Limited Equity 7,342,008 462,546,474 1.000 High Activity
3417 A GX HSTCC Mirae Asset Global Investments (Hong Kong) Limited Equity 6,590,576 415,206,276 1.000 High Activity
2823 ISHARES A50 BlackRock Asset Management North Asia Limited Equity 6,157,102 387,897,412 1.000 High Activity
3088 CAM HS TECH China Asset Management (Hong Kong) Limited Equity 5,573,622 351,138,205 1.000 High Activity
3466 HS HIGH DIV Hang Seng Investment Management Limited Equity 4,611,235 290,507,815 1.000 High Activity
2802 A CSOP HSCEICC CSOP Asset Management Limited Equity 3,013,900 15,069,500 1.000 High Activity
2822 CSOP A50 ETF CSOP Asset Management Limited Equity 2,790,634 175,809,938 1.000 High Activity
7288 FL2 CSOP HSCEI CSOP Asset Management Limited Equity 2,544,044 160,274,800 1.000 High Activity
7588 FI2 CSOP HSCEI CSOP Asset Management Limited Equity 2,435,730 153,451,004 1.000 High Activity

Report generated: 2025-12-18 12:36:22.881685