Skip to content

Commit 8d4c3de

Browse files
feat: add commodity ETF ingestion universe (#123)
1 parent e000546 commit 8d4c3de

16 files changed

Lines changed: 162 additions & 9 deletions

File tree

dbt_project/macros/data_quality_helpers.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{% set tables = [
33
'us_sector_etfs_raw',
44
'currency_etfs_raw',
5+
'commodity_etfs_raw',
56
'major_indices_raw',
67
'fixed_income_etfs_raw',
78
'global_markets_raw',

dbt_project/models/markets/technical/schema.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ models:
2525
- major_index
2626
- fixed_income_etf
2727
- currency_etf
28+
- commodity_etf
2829
- global_market
2930
- name: symbol
3031
tests:

dbt_project/models/markets/technical/technical_price_universe.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
{'model': 'stg_major_indices', 'universe': 'major_index'},
2424
{'model': 'stg_fixed_income', 'universe': 'fixed_income_etf'},
2525
{'model': 'stg_currency', 'universe': 'currency_etf'},
26+
{'model': 'stg_commodity_etfs', 'universe': 'commodity_etf'},
2627
{'model': 'stg_global_markets', 'universe': 'global_market'},
2728
] %}
2829

dbt_project/models/semantic_layer/current_data_coverage.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ WITH source_specs AS (
66
STRUCT('sp500_companies_prices_raw' AS source_name, 'markets' AS source_domain, 'daily_market_prices' AS grain, 31 AS lookback_days, 5 AS freshness_warn_days, 10 AS freshness_error_days),
77
STRUCT('us_sector_etfs_raw' AS source_name, 'markets' AS source_domain, 'daily_market_prices' AS grain, 31 AS lookback_days, 5 AS freshness_warn_days, 10 AS freshness_error_days),
88
STRUCT('currency_etfs_raw' AS source_name, 'markets' AS source_domain, 'daily_market_prices' AS grain, 31 AS lookback_days, 5 AS freshness_warn_days, 10 AS freshness_error_days),
9+
STRUCT('commodity_etfs_raw' AS source_name, 'markets' AS source_domain, 'daily_market_prices' AS grain, 31 AS lookback_days, 5 AS freshness_warn_days, 10 AS freshness_error_days),
910
STRUCT('major_indices_raw' AS source_name, 'markets' AS source_domain, 'daily_market_prices' AS grain, 31 AS lookback_days, 5 AS freshness_warn_days, 10 AS freshness_error_days),
1011
STRUCT('fixed_income_etfs_raw' AS source_name, 'markets' AS source_domain, 'daily_market_prices' AS grain, 31 AS lookback_days, 5 AS freshness_warn_days, 10 AS freshness_error_days),
1112
STRUCT('global_markets_raw' AS source_name, 'markets' AS source_domain, 'daily_market_prices' AS grain, 31 AS lookback_days, 5 AS freshness_warn_days, 10 AS freshness_error_days),
@@ -44,6 +45,15 @@ raw_source_observations AS (
4445

4546
UNION ALL
4647

48+
SELECT
49+
'commodity_etfs_raw' AS source_name,
50+
symbol AS entity_id,
51+
date AS observation_date
52+
FROM {{ ref('stg_commodity_etfs') }}
53+
WHERE symbol IS NOT NULL AND date IS NOT NULL
54+
55+
UNION ALL
56+
4757
SELECT
4858
'major_indices_raw' AS source_name,
4959
symbol AS entity_id,

dbt_project/models/sources.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ sources:
5353
freshness:
5454
warn_after: {count: 5, period: day}
5555
error_after: {count: 10, period: day}
56+
- name: commodity_etfs_raw
57+
config:
58+
loaded_at_field: date
59+
freshness:
60+
warn_after: {count: 5, period: day}
61+
error_after: {count: 10, period: day}
5662
- name: major_indices_raw
5763
config:
5864
loaded_at_field: date

dbt_project/models/staging/schema.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,25 @@ models:
7878
tests:
7979
- not_null
8080

81+
- name: stg_commodity_etfs
82+
description: >
83+
raw data from MarketStack API for Commodity ETFs
84+
tests:
85+
- ohlc_consistency:
86+
config:
87+
severity: warn
88+
- usd_currency_only:
89+
column_name: price_currency
90+
config:
91+
severity: warn
92+
columns:
93+
- name: date
94+
tests:
95+
- not_null
96+
- name: symbol
97+
tests:
98+
- not_null
99+
81100
- name: stg_major_indices
82101
description: >
83102
raw data from MarketStack API for Major Indices
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
SELECT
2+
open,
3+
high,
4+
low,
5+
close,
6+
volume,
7+
adj_high,
8+
adj_low,
9+
adj_close,
10+
adj_open,
11+
adj_volume,
12+
split_factor,
13+
dividend,
14+
name,
15+
exchange_code,
16+
asset_type,
17+
price_currency,
18+
symbol,
19+
exchange,
20+
SAFE_CAST(SUBSTR(CAST(date AS STRING), 1, 10) AS DATE) AS date
21+
FROM {{ source('staging', 'commodity_etfs_raw') }}

macro_agents/src/macro_agents/defs/constants/market_stack_constants.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@
5454
# IBIT.INDX removed - MarketStack returns 422 (ticker dot notation unsupported)
5555
]
5656

57+
# Commodity ETFs spanning precious metals, energy, agriculture, and broad baskets.
58+
COMMODITY_ETFS = [
59+
"GLD", # SPDR Gold Shares
60+
"SLV", # iShares Silver Trust
61+
"USO", # United States Oil Fund
62+
"UNG", # United States Natural Gas Fund
63+
"DBA", # Invesco DB Agriculture Fund
64+
"DBC", # Invesco DB Commodity Index Tracking Fund
65+
"CORN", # Teucrium Corn Fund
66+
"WEAT", # Teucrium Wheat Fund
67+
"SOYB", # Teucrium Soybean Fund
68+
"CPER", # United States Copper Index Fund
69+
"PPLT", # abrdn Physical Platinum Shares ETF
70+
"PALL", # abrdn Physical Palladium Shares ETF
71+
]
72+
5773
# Energy commodities (requires premium plan)
5874
ENERGY_COMMODITIES = [
5975
"brent",
@@ -123,6 +139,7 @@
123139
MAJOR_INDICES_TICKERS,
124140
US_SECTOR_ETFS,
125141
FIXED_INCOME_ETFS,
142+
COMMODITY_ETFS,
126143
]
127144

128145
ALL_COMMODITIES = ENERGY_COMMODITIES + INPUT_COMMODITIES + AGRICULTURE_COMMODITIES

macro_agents/src/macro_agents/defs/domains/markets/assets.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
COMMODITIES_GROUP,
1212
MARKETS_GROUP,
1313
agriculture_commodities_partitions,
14+
commodity_etfs_partitions,
1415
currency_etfs_partitions,
1516
energy_commodities_partitions,
1617
fixed_income_etfs_partitions,
@@ -495,6 +496,24 @@ def currency_etfs_raw(
495496
return _fetch_ticker_partitions(context, bq, marketstack, "currency_etfs_raw")
496497

497498

499+
@dg.asset(
500+
group_name=MARKETS_GROUP,
501+
kinds={"polars", "duckdb"},
502+
partitions_def=commodity_etfs_partitions,
503+
backfill_policy=dg.BackfillPolicy.multi_run(max_partitions_per_run=50),
504+
automation_condition=dg.AutomationCondition.on_cron(
505+
"45 18 * * 5", "America/New_York"
506+
),
507+
description="Raw data from MarketStack API for Commodity ETFs",
508+
)
509+
def commodity_etfs_raw(
510+
context: dg.AssetExecutionContext,
511+
bq: BigQueryWarehouseResource,
512+
marketstack: MarketStackResource,
513+
) -> dg.MaterializeResult:
514+
return _fetch_ticker_partitions(context, bq, marketstack, "commodity_etfs_raw")
515+
516+
498517
@dg.asset(
499518
group_name=MARKETS_GROUP,
500519
kinds={"polars", "duckdb"},

macro_agents/src/macro_agents/defs/domains/markets/checks.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from macro_agents.defs.domains.markets.assets import (
66
agriculture_commodities_raw,
7+
commodity_etfs_raw,
78
currency_etfs_raw,
89
energy_commodities_raw,
910
fixed_income_etfs_raw,
@@ -44,19 +45,16 @@ def check_weekly_data_coverage(
4445
query = f"""
4546
WITH date_range AS (
4647
SELECT DISTINCT
47-
DATE_TRUNC('week', date_col) as week_start
48-
FROM (
49-
SELECT CAST('{one_year_ago}' AS DATE) + (INTERVAL '1 day' * days) as date_col
50-
FROM (SELECT * FROM generate_series(0, 365)) AS t(days)
51-
)
48+
DATE_TRUNC(date_col, WEEK(MONDAY)) AS week_start
49+
FROM UNNEST(GENERATE_DATE_ARRAY(DATE('{one_year_ago}'), DATE('{today}'))) AS date_col
5250
),
5351
data_weeks AS (
5452
SELECT DISTINCT
5553
{partition_column},
56-
DATE_TRUNC('week', CAST({date_column} AS DATE)) as week_start
54+
DATE_TRUNC(SAFE_CAST({date_column} AS DATE), WEEK(MONDAY)) AS week_start
5755
FROM {table_name}
58-
WHERE CAST({date_column} AS DATE) >= '{one_year_ago}'
59-
AND CAST({date_column} AS DATE) <= '{today}'
56+
WHERE SAFE_CAST({date_column} AS DATE) >= DATE('{one_year_ago}')
57+
AND SAFE_CAST({date_column} AS DATE) <= DATE('{today}')
6058
),
6159
expected_weeks AS (
6260
SELECT DISTINCT
@@ -69,7 +67,7 @@ def check_weekly_data_coverage(
6967
SELECT
7068
ew.{partition_column},
7169
ew.week_start,
72-
CAST(ew.week_start AS VARCHAR) || ' to ' || CAST(ew.week_start + INTERVAL '6 days' AS VARCHAR) as week_range
70+
CAST(ew.week_start AS STRING) || ' to ' || CAST(DATE_ADD(ew.week_start, INTERVAL 6 DAY) AS STRING) AS week_range
7371
FROM expected_weeks ew
7472
LEFT JOIN data_weeks dw
7573
ON ew.{partition_column} = dw.{partition_column}
@@ -168,6 +166,19 @@ def currency_etfs_weekly_coverage_check(
168166
)
169167

170168

169+
@dg.asset_check(asset=commodity_etfs_raw)
170+
def commodity_etfs_weekly_coverage_check(
171+
bq: BigQueryWarehouseResource,
172+
) -> dg.AssetCheckResult:
173+
"""Check if every ticker has at least one value per week in the last year."""
174+
return check_weekly_data_coverage(
175+
"commodity_etfs_raw",
176+
"symbol",
177+
"date",
178+
bq,
179+
)
180+
181+
171182
@dg.asset_check(asset=major_indices_raw)
172183
def major_indices_weekly_coverage_check(
173184
bq: BigQueryWarehouseResource,

0 commit comments

Comments
 (0)