Skip to content

Commit a732e7a

Browse files
feat: add factor ETF correlation metrics
1 parent b72e42b commit a732e7a

10 files changed

Lines changed: 589 additions & 9 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ calculate_market_analysis_return('stg_factor_etfs') }}
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
{{ config(
2+
description='Latest factor ETF performance and rolling correlation to sector, style, and thematic ETFs'
3+
) }}
4+
5+
WITH factor_metadata AS (
6+
SELECT *
7+
FROM UNNEST([
8+
STRUCT('VLUE' AS factor_symbol, 'value' AS factor_name),
9+
STRUCT('QUAL' AS factor_symbol, 'quality' AS factor_name),
10+
STRUCT('MTUM' AS factor_symbol, 'momentum' AS factor_name),
11+
STRUCT('SIZE' AS factor_symbol, 'size' AS factor_name),
12+
STRUCT('USMV' AS factor_symbol, 'minimum_volatility' AS factor_name)
13+
])
14+
),
15+
16+
comparison_metadata AS (
17+
SELECT *
18+
FROM UNNEST([
19+
STRUCT('XLK' AS comparison_symbol, 'sector_etf' AS comparison_universe, 'Technology' AS comparison_name),
20+
STRUCT('XLC' AS comparison_symbol, 'sector_etf' AS comparison_universe, 'Communication Services' AS comparison_name),
21+
STRUCT('XLY' AS comparison_symbol, 'sector_etf' AS comparison_universe, 'Consumer Discretionary' AS comparison_name),
22+
STRUCT('XLF' AS comparison_symbol, 'sector_etf' AS comparison_universe, 'Financials' AS comparison_name),
23+
STRUCT('XLI' AS comparison_symbol, 'sector_etf' AS comparison_universe, 'Industrials' AS comparison_name),
24+
STRUCT('XLU' AS comparison_symbol, 'sector_etf' AS comparison_universe, 'Utilities' AS comparison_name),
25+
STRUCT('XLP' AS comparison_symbol, 'sector_etf' AS comparison_universe, 'Consumer Staples' AS comparison_name),
26+
STRUCT('XLRE' AS comparison_symbol, 'sector_etf' AS comparison_universe, 'Real Estate' AS comparison_name),
27+
STRUCT('XLB' AS comparison_symbol, 'sector_etf' AS comparison_universe, 'Materials' AS comparison_name),
28+
STRUCT('XLE' AS comparison_symbol, 'sector_etf' AS comparison_universe, 'Energy' AS comparison_name),
29+
STRUCT('XLV' AS comparison_symbol, 'sector_etf' AS comparison_universe, 'Health Care' AS comparison_name),
30+
STRUCT('SPY' AS comparison_symbol, 'broad_market_etf' AS comparison_universe, 'S&P 500' AS comparison_name),
31+
STRUCT('QQQ' AS comparison_symbol, 'broad_market_etf' AS comparison_universe, 'Nasdaq 100' AS comparison_name),
32+
STRUCT('DIA' AS comparison_symbol, 'broad_market_etf' AS comparison_universe, 'Dow Jones Industrial Average' AS comparison_name),
33+
STRUCT('RSP' AS comparison_symbol, 'broad_market_etf' AS comparison_universe, 'S&P 500 Equal Weight' AS comparison_name),
34+
STRUCT('IWM' AS comparison_symbol, 'style_etf' AS comparison_universe, 'Russell 2000' AS comparison_name),
35+
STRUCT('IWD' AS comparison_symbol, 'style_etf' AS comparison_universe, 'Russell 1000 Value' AS comparison_name),
36+
STRUCT('IWF' AS comparison_symbol, 'style_etf' AS comparison_universe, 'Russell 1000 Growth' AS comparison_name),
37+
STRUCT('IYT' AS comparison_symbol, 'thematic_etf' AS comparison_universe, 'Transportation' AS comparison_name),
38+
STRUCT('SOXX' AS comparison_symbol, 'thematic_etf' AS comparison_universe, 'Semiconductors' AS comparison_name)
39+
])
40+
),
41+
42+
factor_prices AS (
43+
SELECT
44+
symbol AS factor_symbol,
45+
exchange,
46+
date,
47+
adj_close,
48+
SAFE_DIVIDE(
49+
adj_close - LAG(adj_close) OVER (
50+
PARTITION BY symbol, exchange
51+
ORDER BY date
52+
),
53+
LAG(adj_close) OVER (
54+
PARTITION BY symbol, exchange
55+
ORDER BY date
56+
)
57+
) AS factor_daily_return
58+
FROM {{ ref('stg_factor_etfs') }}
59+
WHERE adj_close IS NOT NULL
60+
),
61+
62+
sector_prices AS (
63+
SELECT
64+
symbol AS comparison_symbol,
65+
exchange,
66+
date,
67+
adj_close,
68+
SAFE_DIVIDE(
69+
adj_close - LAG(adj_close) OVER (
70+
PARTITION BY symbol, exchange
71+
ORDER BY date
72+
),
73+
LAG(adj_close) OVER (
74+
PARTITION BY symbol, exchange
75+
ORDER BY date
76+
)
77+
) AS comparison_daily_return
78+
FROM {{ ref('stg_us_sectors') }}
79+
WHERE adj_close IS NOT NULL
80+
),
81+
82+
major_index_prices AS (
83+
SELECT
84+
major_indices.symbol AS comparison_symbol,
85+
major_indices.exchange,
86+
major_indices.date,
87+
major_indices.adj_close,
88+
SAFE_DIVIDE(
89+
major_indices.adj_close - LAG(major_indices.adj_close) OVER (
90+
PARTITION BY major_indices.symbol, major_indices.exchange
91+
ORDER BY major_indices.date
92+
),
93+
LAG(major_indices.adj_close) OVER (
94+
PARTITION BY major_indices.symbol, major_indices.exchange
95+
ORDER BY major_indices.date
96+
)
97+
) AS comparison_daily_return
98+
FROM {{ ref('stg_major_indices') }} AS major_indices
99+
INNER JOIN comparison_metadata
100+
ON major_indices.symbol = comparison_metadata.comparison_symbol
101+
WHERE major_indices.adj_close IS NOT NULL
102+
AND comparison_metadata.comparison_universe != 'sector_etf'
103+
),
104+
105+
comparison_prices AS (
106+
SELECT
107+
comparison_symbol,
108+
exchange,
109+
date,
110+
adj_close,
111+
comparison_daily_return
112+
FROM sector_prices
113+
UNION ALL
114+
SELECT
115+
comparison_symbol,
116+
exchange,
117+
date,
118+
adj_close,
119+
comparison_daily_return
120+
FROM major_index_prices
121+
),
122+
123+
latest_common_date AS (
124+
SELECT MAX(factor_prices.date) AS as_of_date
125+
FROM factor_prices
126+
INNER JOIN comparison_prices
127+
ON factor_prices.date = comparison_prices.date
128+
),
129+
130+
joined_returns AS (
131+
SELECT
132+
factor_prices.factor_symbol,
133+
comparison_prices.comparison_symbol,
134+
factor_prices.date,
135+
factor_prices.factor_daily_return,
136+
comparison_prices.comparison_daily_return
137+
FROM factor_prices
138+
INNER JOIN comparison_prices
139+
ON factor_prices.date = comparison_prices.date
140+
CROSS JOIN latest_common_date AS latest
141+
WHERE factor_prices.factor_daily_return IS NOT NULL
142+
AND comparison_prices.comparison_daily_return IS NOT NULL
143+
AND factor_prices.date BETWEEN DATE_SUB(latest.as_of_date, INTERVAL 365 DAY)
144+
AND latest.as_of_date
145+
),
146+
147+
rolling_correlations AS (
148+
SELECT
149+
latest.as_of_date,
150+
joined_returns.factor_symbol,
151+
joined_returns.comparison_symbol,
152+
COUNTIF(
153+
joined_returns.date >= DATE_SUB(latest.as_of_date, INTERVAL 90 DAY)
154+
) AS observations_3mo,
155+
ROUND(CORR(
156+
CASE
157+
WHEN joined_returns.date >= DATE_SUB(latest.as_of_date, INTERVAL 90 DAY)
158+
THEN joined_returns.factor_daily_return
159+
END,
160+
CASE
161+
WHEN joined_returns.date >= DATE_SUB(latest.as_of_date, INTERVAL 90 DAY)
162+
THEN joined_returns.comparison_daily_return
163+
END
164+
), 4) AS corr_3mo,
165+
COUNT(*) AS observations_1yr,
166+
ROUND(CORR(joined_returns.factor_daily_return, joined_returns.comparison_daily_return), 4) AS corr_1yr
167+
FROM joined_returns
168+
CROSS JOIN latest_common_date AS latest
169+
GROUP BY latest.as_of_date, joined_returns.factor_symbol, joined_returns.comparison_symbol
170+
),
171+
172+
latest_factor_performance AS (
173+
SELECT
174+
symbol AS factor_symbol,
175+
date,
176+
pct_change_1mo,
177+
pct_change_3mo,
178+
pct_change_1yr,
179+
std_diff_1yr
180+
FROM {{ ref('factor_analysis_return') }}
181+
QUALIFY ROW_NUMBER() OVER (
182+
PARTITION BY symbol
183+
ORDER BY date DESC
184+
) = 1
185+
),
186+
187+
sector_performance AS (
188+
SELECT
189+
symbol AS comparison_symbol,
190+
date,
191+
pct_change_1mo,
192+
pct_change_3mo,
193+
pct_change_1yr,
194+
std_diff_1yr
195+
FROM {{ ref('us_sector_analysis_return') }}
196+
),
197+
198+
major_index_performance AS (
199+
SELECT
200+
major_indices.symbol AS comparison_symbol,
201+
major_indices.date,
202+
major_indices.pct_change_1mo,
203+
major_indices.pct_change_3mo,
204+
major_indices.pct_change_1yr,
205+
major_indices.std_diff_1yr
206+
FROM {{ ref('major_indicies_analysis_return') }} AS major_indices
207+
INNER JOIN comparison_metadata
208+
ON major_indices.symbol = comparison_metadata.comparison_symbol
209+
WHERE comparison_metadata.comparison_universe != 'sector_etf'
210+
),
211+
212+
comparison_performance AS (
213+
SELECT
214+
comparison_symbol,
215+
date,
216+
pct_change_1mo,
217+
pct_change_3mo,
218+
pct_change_1yr,
219+
std_diff_1yr
220+
FROM sector_performance
221+
UNION ALL
222+
SELECT
223+
comparison_symbol,
224+
date,
225+
pct_change_1mo,
226+
pct_change_3mo,
227+
pct_change_1yr,
228+
std_diff_1yr
229+
FROM major_index_performance
230+
),
231+
232+
latest_comparison_performance AS (
233+
SELECT
234+
comparison_symbol,
235+
date,
236+
pct_change_1mo,
237+
pct_change_3mo,
238+
pct_change_1yr,
239+
std_diff_1yr
240+
FROM comparison_performance
241+
QUALIFY ROW_NUMBER() OVER (
242+
PARTITION BY comparison_symbol
243+
ORDER BY date DESC
244+
) = 1
245+
)
246+
247+
SELECT
248+
CONCAT(
249+
correlations.factor_symbol,
250+
':',
251+
correlations.comparison_symbol,
252+
':',
253+
CAST(correlations.as_of_date AS STRING)
254+
) AS factor_sector_key,
255+
correlations.as_of_date,
256+
correlations.factor_symbol,
257+
factor_metadata.factor_name,
258+
correlations.comparison_symbol,
259+
comparison_metadata.comparison_name,
260+
comparison_metadata.comparison_universe,
261+
correlations.comparison_symbol AS sector_symbol,
262+
comparison_metadata.comparison_name AS sector_name,
263+
correlations.observations_3mo,
264+
correlations.corr_3mo,
265+
correlations.observations_1yr,
266+
correlations.corr_1yr,
267+
factor_performance.pct_change_1mo AS factor_return_1mo,
268+
factor_performance.pct_change_3mo AS factor_return_3mo,
269+
factor_performance.pct_change_1yr AS factor_return_1yr,
270+
comparison_performance.pct_change_1mo AS sector_return_1mo,
271+
comparison_performance.pct_change_3mo AS sector_return_3mo,
272+
comparison_performance.pct_change_1yr AS sector_return_1yr,
273+
ROUND(factor_performance.pct_change_3mo - comparison_performance.pct_change_3mo, 2) AS factor_sector_return_spread_3mo,
274+
ROUND(factor_performance.pct_change_1yr - comparison_performance.pct_change_1yr, 2) AS factor_sector_return_spread_1yr,
275+
factor_performance.std_diff_1yr AS factor_volatility_proxy_1yr,
276+
comparison_performance.std_diff_1yr AS sector_volatility_proxy_1yr
277+
FROM rolling_correlations AS correlations
278+
LEFT JOIN factor_metadata
279+
ON correlations.factor_symbol = factor_metadata.factor_symbol
280+
LEFT JOIN comparison_metadata
281+
ON correlations.comparison_symbol = comparison_metadata.comparison_symbol
282+
LEFT JOIN latest_factor_performance AS factor_performance
283+
ON correlations.factor_symbol = factor_performance.factor_symbol
284+
LEFT JOIN latest_comparison_performance AS comparison_performance
285+
ON correlations.comparison_symbol = comparison_performance.comparison_symbol

dbt_project/models/markets/schema.yml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,99 @@ models:
341341
tests:
342342
- not_null
343343

344+
- name: factor_analysis_return
345+
description: >
346+
Daily OHLCV price data and rolling returns for US equity factor ETFs
347+
covering value, quality, momentum, size, and minimum volatility.
348+
Grain: one row per factor ETF symbol per exchange per trading day.
349+
tests:
350+
- unique_combination:
351+
arguments:
352+
combination_of_columns: ['symbol', 'exchange', 'date']
353+
columns:
354+
- name: symbol
355+
description: Factor ETF symbol.
356+
tests:
357+
- not_null
358+
- name: exchange
359+
description: Exchange where the ETF is traded.
360+
tests:
361+
- not_null
362+
- name: date
363+
description: Trading date.
364+
tests:
365+
- not_null
366+
- name: current_price
367+
description: Current adjusted close price.
368+
- name: current_volume
369+
description: Current day's trading volume.
370+
- name: pct_change_1mo
371+
description: Percentage change from 1 month ago to current price.
372+
- name: pct_change_3mo
373+
description: Percentage change from 3 months ago to current price.
374+
- name: pct_change_1yr
375+
description: Percentage change from 1 year ago to current price.
376+
- name: std_diff_1yr
377+
description: Standard deviation of daily price differences over the past 1 year.
378+
379+
- name: factor_sector_correlation
380+
description: >
381+
Latest rolling correlations and relative performance spreads between
382+
US equity factor ETFs and sector, broad market, style, and thematic ETFs.
383+
Supports factor rotation, sector exposure, and thematic relationship analysis.
384+
tests:
385+
- unique_combination:
386+
arguments:
387+
combination_of_columns: ['factor_sector_key']
388+
columns:
389+
- name: factor_sector_key
390+
description: Stable key for factor, sector, and as-of date.
391+
tests:
392+
- not_null
393+
- unique
394+
- name: as_of_date
395+
description: Latest common trading date used for the correlation window.
396+
tests:
397+
- not_null
398+
- name: factor_symbol
399+
description: Factor ETF ticker symbol.
400+
tests:
401+
- not_null
402+
- name: factor_name
403+
description: Factor exposure represented by the ETF.
404+
tests:
405+
- not_null
406+
- accepted_values:
407+
arguments:
408+
values: ['value', 'quality', 'momentum', 'size', 'minimum_volatility']
409+
- name: comparison_symbol
410+
description: Compared ETF ticker symbol.
411+
tests:
412+
- not_null
413+
- name: comparison_name
414+
description: Human-readable compared ETF name.
415+
- name: comparison_universe
416+
description: Compared ETF universe.
417+
tests:
418+
- not_null
419+
- accepted_values:
420+
arguments:
421+
values: ['sector_etf', 'broad_market_etf', 'style_etf', 'thematic_etf']
422+
- name: sector_symbol
423+
description: Backward-compatible compared ETF ticker symbol.
424+
tests:
425+
- not_null
426+
- name: sector_name
427+
description: Backward-compatible compared ETF name.
428+
- name: corr_3mo
429+
description: Rolling 3-month daily-return correlation between the factor ETF and compared ETF.
430+
- name: corr_1yr
431+
description: Rolling 1-year daily-return correlation between the factor ETF and compared ETF.
432+
- name: factor_sector_return_spread_3mo
433+
description: Factor ETF 3-month return minus compared ETF 3-month return.
434+
- name: factor_sector_return_spread_1yr
435+
description: Factor ETF 1-year return minus compared ETF 1-year return.
436+
344437
- name: sp500_companies_analysis_return
345438
description: >
346439
Daily OHLCV price data and rolling returns for S&P 500 companies.

dbt_project/models/markets/technical/schema.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ models:
2626
- fixed_income_etf
2727
- currency_etf
2828
- commodity_etf
29+
- factor_etf
2930
- global_market
3031
- name: symbol
3132
tests:

0 commit comments

Comments
 (0)