# 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)
)
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
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))
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)
)
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"
)
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"
)
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"
)
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"
)
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"
)
}
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")
)
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"
)
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 |
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 |
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_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