feat(extract): add OpenWeatherMap daily weather extractor
Adds extract/openweathermap package with daily weather extraction for 8
coffee-growing regions (Brazil, Vietnam, Colombia, Ethiopia, Honduras,
Guatemala, Indonesia). Feeds crop stress signal for commodity sentiment score.
Extractor:
- OWM One Call API 3.0 / Day Summary — one JSON.gz per (location, date)
- extract_weather: daily, fetches yesterday + today (16 calls max)
- extract_weather_backfill: fills 2020-01-01 to yesterday, capped at 500
calls/run with resume cursor '{location_id}:{date}' for crash safety
- Full idempotency via file existence check; state tracking via extract_core
SQLMesh:
- seeds.weather_locations (8 regions with lat/lon/variety)
- foundation.fct_weather_daily: INCREMENTAL_BY_TIME_RANGE, grain
(location_id, observation_date), dedup via hash key, crop stress flags:
is_frost (<2°C), is_heat_stress (>35°C), is_drought (<1mm), in_growing_season
Landing path: LANDING_DIR/weather/{location_id}/{year}/{date}.json.gz
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,12 @@
|
||||
-- Serving mart: KC=F Coffee C futures prices, analytics-ready.
|
||||
--
|
||||
-- Adds moving averages (20-day, 50-day SMA) and 52-week high/low range.
|
||||
-- Filtered to trading days only (NULL close rows excluded upstream).
|
||||
--
|
||||
-- Grain: one row per trade_date.
|
||||
|
||||
/* Serving mart: KC=F Coffee C futures prices, analytics-ready. */ /* Adds moving averages (20-day, 50-day SMA) and 52-week high/low range. */ /* Filtered to trading days only (NULL close rows excluded upstream). */ /* Grain: one row per trade_date. */
|
||||
MODEL (
|
||||
name serving.coffee_prices,
|
||||
kind INCREMENTAL_BY_TIME_RANGE (
|
||||
time_column trade_date
|
||||
),
|
||||
grain (trade_date),
|
||||
grain (
|
||||
trade_date
|
||||
),
|
||||
start '1971-08-16',
|
||||
cron '@daily'
|
||||
);
|
||||
@@ -24,38 +20,26 @@ WITH base AS (
|
||||
f.close,
|
||||
f.adj_close,
|
||||
f.volume,
|
||||
|
||||
-- Daily return: (close - prev_close) / prev_close * 100
|
||||
round(
|
||||
(f.close - LAG(f.close, 1) OVER (ORDER BY f.trade_date))
|
||||
/ NULLIF(LAG(f.close, 1) OVER (ORDER BY f.trade_date), 0) * 100,
|
||||
ROUND(
|
||||
(
|
||||
f.close - LAG(f.close, 1) OVER (ORDER BY f.trade_date)
|
||||
) / NULLIF(LAG(f.close, 1) OVER (ORDER BY f.trade_date), 0) * 100,
|
||||
4
|
||||
) AS daily_return_pct,
|
||||
|
||||
-- 20-day simple moving average (1 trading month)
|
||||
round(
|
||||
) AS daily_return_pct, /* Daily return: (close - prev_close) / prev_close * 100 */
|
||||
ROUND(
|
||||
AVG(f.close) OVER (ORDER BY f.trade_date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW),
|
||||
4
|
||||
) AS sma_20d,
|
||||
|
||||
-- 50-day simple moving average (2.5 trading months)
|
||||
round(
|
||||
) AS sma_20d, /* 20-day simple moving average (1 trading month) */
|
||||
ROUND(
|
||||
AVG(f.close) OVER (ORDER BY f.trade_date ROWS BETWEEN 49 PRECEDING AND CURRENT ROW),
|
||||
4
|
||||
) AS sma_50d,
|
||||
|
||||
-- 52-week high (approximately 252 trading days)
|
||||
MAX(f.high) OVER (ORDER BY f.trade_date ROWS BETWEEN 251 PRECEDING AND CURRENT ROW)
|
||||
AS high_52w,
|
||||
|
||||
-- 52-week low
|
||||
MIN(f.low) OVER (ORDER BY f.trade_date ROWS BETWEEN 251 PRECEDING AND CURRENT ROW)
|
||||
AS low_52w
|
||||
|
||||
FROM foundation.fct_coffee_prices f
|
||||
WHERE f.trade_date BETWEEN @start_ds AND @end_ds
|
||||
) AS sma_50d, /* 50-day simple moving average (2.5 trading months) */
|
||||
MAX(f.high) OVER (ORDER BY f.trade_date ROWS BETWEEN 251 PRECEDING AND CURRENT ROW) AS high_52w, /* 52-week high (approximately 252 trading days) */
|
||||
MIN(f.low) OVER (ORDER BY f.trade_date ROWS BETWEEN 251 PRECEDING AND CURRENT ROW) AS low_52w /* 52-week low */
|
||||
FROM foundation.fct_coffee_prices AS f
|
||||
WHERE
|
||||
f.trade_date BETWEEN @start_ds AND @end_ds
|
||||
)
|
||||
|
||||
SELECT
|
||||
b.trade_date,
|
||||
d.commodity_name,
|
||||
@@ -71,7 +55,9 @@ SELECT
|
||||
b.sma_50d,
|
||||
b.high_52w,
|
||||
b.low_52w
|
||||
FROM base b
|
||||
CROSS JOIN foundation.dim_commodity d
|
||||
WHERE d.ticker = 'KC=F'
|
||||
ORDER BY b.trade_date
|
||||
FROM base AS b
|
||||
CROSS JOIN foundation.dim_commodity AS d
|
||||
WHERE
|
||||
d.ticker = 'KC=F'
|
||||
ORDER BY
|
||||
b.trade_date
|
||||
@@ -1,60 +1,51 @@
|
||||
-- Serving mart: ICE certified Coffee C stock aging report, analytics-ready.
|
||||
--
|
||||
-- Shows the age distribution of certified stocks across delivery ports.
|
||||
-- Age buckets represent how long coffee has been in certified storage.
|
||||
-- Older stock approaching certificate limits is a supply quality signal.
|
||||
--
|
||||
-- Source: ICE Certified Stock Aging Report (monthly)
|
||||
-- Grain: one row per (report_date, age_bucket).
|
||||
|
||||
MODEL (
|
||||
name serving.ice_aging_stocks,
|
||||
kind INCREMENTAL_BY_TIME_RANGE (
|
||||
time_column report_date
|
||||
),
|
||||
grain (report_date, age_bucket),
|
||||
start '2020-01-01',
|
||||
cron '@daily'
|
||||
);
|
||||
|
||||
WITH base AS (
|
||||
SELECT
|
||||
f.report_date,
|
||||
f.age_bucket,
|
||||
|
||||
-- Parse age range from "0000 to 0120" format for correct sort order
|
||||
TRY_CAST(split_part(f.age_bucket, ' to ', 1) AS int) AS age_bucket_start_days,
|
||||
TRY_CAST(split_part(f.age_bucket, ' to ', 2) AS int) AS age_bucket_end_days,
|
||||
|
||||
f.antwerp_bags,
|
||||
f.hamburg_bremen_bags,
|
||||
f.houston_bags,
|
||||
f.miami_bags,
|
||||
f.new_orleans_bags,
|
||||
f.new_york_bags,
|
||||
f.total_bags,
|
||||
|
||||
f.source_file
|
||||
FROM foundation.fct_ice_aging_stocks f
|
||||
WHERE f.report_date BETWEEN @start_ds AND @end_ds
|
||||
)
|
||||
|
||||
SELECT
|
||||
b.report_date,
|
||||
d.commodity_name,
|
||||
d.ice_stock_report_code,
|
||||
b.age_bucket,
|
||||
b.age_bucket_start_days,
|
||||
b.age_bucket_end_days,
|
||||
b.antwerp_bags,
|
||||
b.hamburg_bremen_bags,
|
||||
b.houston_bags,
|
||||
b.miami_bags,
|
||||
b.new_orleans_bags,
|
||||
b.new_york_bags,
|
||||
b.total_bags,
|
||||
b.source_file
|
||||
FROM base b
|
||||
CROSS JOIN foundation.dim_commodity d
|
||||
WHERE d.ice_stock_report_code = 'COFFEE-C'
|
||||
ORDER BY b.report_date, b.age_bucket_start_days
|
||||
/* Serving mart: ICE certified Coffee C stock aging report, analytics-ready. */ /* Shows the age distribution of certified stocks across delivery ports. */ /* Age buckets represent how long coffee has been in certified storage. */ /* Older stock approaching certificate limits is a supply quality signal. */ /* Source: ICE Certified Stock Aging Report (monthly) */ /* Grain: one row per (report_date, age_bucket). */
|
||||
MODEL (
|
||||
name serving.ice_aging_stocks,
|
||||
kind INCREMENTAL_BY_TIME_RANGE (
|
||||
time_column report_date
|
||||
),
|
||||
grain (report_date, age_bucket),
|
||||
start '2020-01-01',
|
||||
cron '@daily'
|
||||
);
|
||||
|
||||
WITH base AS (
|
||||
SELECT
|
||||
f.report_date,
|
||||
f.age_bucket,
|
||||
TRY_CAST(SPLIT_PART(f.age_bucket, ' to ', 1) AS INT) AS age_bucket_start_days, /* Parse age range from "0000 to 0120" format for correct sort order */
|
||||
TRY_CAST(SPLIT_PART(f.age_bucket, ' to ', 2) AS INT) AS age_bucket_end_days,
|
||||
f.antwerp_bags,
|
||||
f.hamburg_bremen_bags,
|
||||
f.houston_bags,
|
||||
f.miami_bags,
|
||||
f.new_orleans_bags,
|
||||
f.new_york_bags,
|
||||
f.total_bags,
|
||||
f.source_file
|
||||
FROM foundation.fct_ice_aging_stocks AS f
|
||||
WHERE
|
||||
f.report_date BETWEEN @start_ds AND @end_ds
|
||||
)
|
||||
SELECT
|
||||
b.report_date,
|
||||
d.commodity_name,
|
||||
d.ice_stock_report_code,
|
||||
b.age_bucket,
|
||||
b.age_bucket_start_days,
|
||||
b.age_bucket_end_days,
|
||||
b.antwerp_bags,
|
||||
b.hamburg_bremen_bags,
|
||||
b.houston_bags,
|
||||
b.miami_bags,
|
||||
b.new_orleans_bags,
|
||||
b.new_york_bags,
|
||||
b.total_bags,
|
||||
b.source_file
|
||||
FROM base AS b
|
||||
CROSS JOIN foundation.dim_commodity AS d
|
||||
WHERE
|
||||
d.ice_stock_report_code = 'COFFEE-C'
|
||||
ORDER BY
|
||||
b.report_date,
|
||||
b.age_bucket_start_days
|
||||
@@ -1,19 +1,12 @@
|
||||
-- Serving mart: ICE certified Coffee C warehouse stocks, analytics-ready.
|
||||
--
|
||||
-- Adds 30-day rolling average, week-over-week change, and drawdown from
|
||||
-- 52-week high. Physical supply indicator used alongside S/D and positioning.
|
||||
--
|
||||
-- "Certified stocks" = coffee graded and stamped as eligible for delivery
|
||||
-- against ICE Coffee C futures — traders watch this as a squeeze indicator.
|
||||
--
|
||||
-- Grain: one row per report_date.
|
||||
|
||||
/* Serving mart: ICE certified Coffee C warehouse stocks, analytics-ready. */ /* Adds 30-day rolling average, week-over-week change, and drawdown from */ /* 52-week high. Physical supply indicator used alongside S/D and positioning. */ /* "Certified stocks" = coffee graded and stamped as eligible for delivery */ /* against ICE Coffee C futures — traders watch this as a squeeze indicator. */ /* Grain: one row per report_date. */
|
||||
MODEL (
|
||||
name serving.ice_warehouse_stocks,
|
||||
kind INCREMENTAL_BY_TIME_RANGE (
|
||||
time_column report_date
|
||||
),
|
||||
grain (report_date),
|
||||
grain (
|
||||
report_date
|
||||
),
|
||||
start '2000-01-01',
|
||||
cron '@daily'
|
||||
);
|
||||
@@ -23,45 +16,25 @@ WITH base AS (
|
||||
f.report_date,
|
||||
f.total_certified_bags,
|
||||
f.pending_grading_bags,
|
||||
|
||||
-- Week-over-week change (compare to 7 calendar days ago via LAG over ordered rows)
|
||||
-- Using LAG(1) since data is daily: compares to previous trading/reporting day
|
||||
f.total_certified_bags
|
||||
- LAG(f.total_certified_bags, 1) OVER (ORDER BY f.report_date) AS wow_change_bags,
|
||||
|
||||
-- 30-day rolling average (smooths daily noise)
|
||||
round(
|
||||
AVG(f.total_certified_bags::double) OVER (
|
||||
ORDER BY f.report_date ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
|
||||
),
|
||||
f.total_certified_bags /* Week-over-week change (compare to 7 calendar days ago via LAG over ordered rows) */ /* Using LAG(1) since data is daily: compares to previous trading/reporting day */ - LAG(f.total_certified_bags, 1) OVER (ORDER BY f.report_date) AS wow_change_bags,
|
||||
ROUND(
|
||||
AVG(f.total_certified_bags::DOUBLE) OVER (ORDER BY f.report_date ROWS BETWEEN 29 PRECEDING AND CURRENT ROW),
|
||||
0
|
||||
) AS avg_30d_bags,
|
||||
|
||||
-- 52-week high (365 calendar days ≈ 252 trading days; use 365-row window as proxy)
|
||||
MAX(f.total_certified_bags) OVER (
|
||||
ORDER BY f.report_date ROWS BETWEEN 364 PRECEDING AND CURRENT ROW
|
||||
) AS high_52w_bags,
|
||||
|
||||
-- Drawdown from 52-week high (pct below peak — squeeze indicator)
|
||||
round(
|
||||
(f.total_certified_bags::double
|
||||
- MAX(f.total_certified_bags) OVER (
|
||||
ORDER BY f.report_date ROWS BETWEEN 364 PRECEDING AND CURRENT ROW
|
||||
)::double
|
||||
)
|
||||
/ NULLIF(
|
||||
MAX(f.total_certified_bags) OVER (
|
||||
ORDER BY f.report_date ROWS BETWEEN 364 PRECEDING AND CURRENT ROW
|
||||
)::double,
|
||||
0
|
||||
) * 100,
|
||||
) AS avg_30d_bags, /* 30-day rolling average (smooths daily noise) */
|
||||
MAX(f.total_certified_bags) OVER (ORDER BY f.report_date ROWS BETWEEN 364 PRECEDING AND CURRENT ROW) AS high_52w_bags, /* 52-week high (365 calendar days ≈ 252 trading days; use 365-row window as proxy) */
|
||||
ROUND(
|
||||
(
|
||||
f.total_certified_bags::DOUBLE - MAX(f.total_certified_bags) OVER (ORDER BY f.report_date ROWS BETWEEN 364 PRECEDING AND CURRENT ROW)::DOUBLE
|
||||
) / NULLIF(
|
||||
MAX(f.total_certified_bags) OVER (ORDER BY f.report_date ROWS BETWEEN 364 PRECEDING AND CURRENT ROW)::DOUBLE,
|
||||
0
|
||||
) * 100,
|
||||
2
|
||||
) AS drawdown_from_52w_high_pct
|
||||
|
||||
FROM foundation.fct_ice_warehouse_stocks f
|
||||
WHERE f.report_date BETWEEN @start_ds AND @end_ds
|
||||
) AS drawdown_from_52w_high_pct /* Drawdown from 52-week high (pct below peak — squeeze indicator) */
|
||||
FROM foundation.fct_ice_warehouse_stocks AS f
|
||||
WHERE
|
||||
f.report_date BETWEEN @start_ds AND @end_ds
|
||||
)
|
||||
|
||||
SELECT
|
||||
b.report_date,
|
||||
d.commodity_name,
|
||||
@@ -72,7 +45,9 @@ SELECT
|
||||
b.avg_30d_bags,
|
||||
b.high_52w_bags,
|
||||
b.drawdown_from_52w_high_pct
|
||||
FROM base b
|
||||
CROSS JOIN foundation.dim_commodity d
|
||||
WHERE d.ice_stock_report_code = 'COFFEE-C'
|
||||
ORDER BY b.report_date
|
||||
FROM base AS b
|
||||
CROSS JOIN foundation.dim_commodity AS d
|
||||
WHERE
|
||||
d.ice_stock_report_code = 'COFFEE-C'
|
||||
ORDER BY
|
||||
b.report_date
|
||||
@@ -1,78 +1,64 @@
|
||||
-- Serving mart: ICE certified Coffee C warehouse stocks by port, analytics-ready.
|
||||
--
|
||||
-- End-of-month certified stock levels broken down by delivery port.
|
||||
-- Covers November 1996 to present (~30 years). Useful for understanding
|
||||
-- geographic shifts in the certified supply base over time.
|
||||
--
|
||||
-- Source: ICE historical by-port XLS (EOM_KC_cert_stox_by_port_nov96-present.xls)
|
||||
-- Grain: one row per report_date (end-of-month).
|
||||
|
||||
MODEL (
|
||||
name serving.ice_warehouse_stocks_by_port,
|
||||
kind INCREMENTAL_BY_TIME_RANGE (
|
||||
time_column report_date
|
||||
),
|
||||
grain (report_date),
|
||||
start '1996-11-01',
|
||||
cron '@daily'
|
||||
);
|
||||
|
||||
WITH base AS (
|
||||
SELECT
|
||||
f.report_date,
|
||||
f.new_york_bags,
|
||||
f.new_orleans_bags,
|
||||
f.houston_bags,
|
||||
f.miami_bags,
|
||||
f.antwerp_bags,
|
||||
f.hamburg_bremen_bags,
|
||||
f.barcelona_bags,
|
||||
f.virginia_bags,
|
||||
f.total_bags,
|
||||
|
||||
-- Month-over-month change in total certified bags
|
||||
f.total_bags
|
||||
- LAG(f.total_bags, 1) OVER (ORDER BY f.report_date) AS mom_change_bags,
|
||||
|
||||
-- Month-over-month percent change
|
||||
round(
|
||||
(f.total_bags::double
|
||||
- LAG(f.total_bags, 1) OVER (ORDER BY f.report_date)::double)
|
||||
/ NULLIF(LAG(f.total_bags, 1) OVER (ORDER BY f.report_date)::double, 0) * 100,
|
||||
2
|
||||
) AS mom_change_pct,
|
||||
|
||||
-- 12-month rolling average
|
||||
round(
|
||||
AVG(f.total_bags::double) OVER (
|
||||
ORDER BY f.report_date ROWS BETWEEN 11 PRECEDING AND CURRENT ROW
|
||||
),
|
||||
0
|
||||
) AS avg_12m_bags,
|
||||
|
||||
f.source_file
|
||||
FROM foundation.fct_ice_warehouse_stocks_by_port f
|
||||
WHERE f.report_date BETWEEN @start_ds AND @end_ds
|
||||
)
|
||||
|
||||
SELECT
|
||||
b.report_date,
|
||||
d.commodity_name,
|
||||
d.ice_stock_report_code,
|
||||
b.new_york_bags,
|
||||
b.new_orleans_bags,
|
||||
b.houston_bags,
|
||||
b.miami_bags,
|
||||
b.antwerp_bags,
|
||||
b.hamburg_bremen_bags,
|
||||
b.barcelona_bags,
|
||||
b.virginia_bags,
|
||||
b.total_bags,
|
||||
b.mom_change_bags,
|
||||
b.mom_change_pct,
|
||||
b.avg_12m_bags,
|
||||
b.source_file
|
||||
FROM base b
|
||||
CROSS JOIN foundation.dim_commodity d
|
||||
WHERE d.ice_stock_report_code = 'COFFEE-C'
|
||||
ORDER BY b.report_date
|
||||
/* Serving mart: ICE certified Coffee C warehouse stocks by port, analytics-ready. */ /* End-of-month certified stock levels broken down by delivery port. */ /* Covers November 1996 to present (~30 years). Useful for understanding */ /* geographic shifts in the certified supply base over time. */ /* Source: ICE historical by-port XLS (EOM_KC_cert_stox_by_port_nov96-present.xls) */ /* Grain: one row per report_date (end-of-month). */
|
||||
MODEL (
|
||||
name serving.ice_warehouse_stocks_by_port,
|
||||
kind INCREMENTAL_BY_TIME_RANGE (
|
||||
time_column report_date
|
||||
),
|
||||
grain (
|
||||
report_date
|
||||
),
|
||||
start '1996-11-01',
|
||||
cron '@daily'
|
||||
);
|
||||
|
||||
WITH base AS (
|
||||
SELECT
|
||||
f.report_date,
|
||||
f.new_york_bags,
|
||||
f.new_orleans_bags,
|
||||
f.houston_bags,
|
||||
f.miami_bags,
|
||||
f.antwerp_bags,
|
||||
f.hamburg_bremen_bags,
|
||||
f.barcelona_bags,
|
||||
f.virginia_bags,
|
||||
f.total_bags,
|
||||
f.total_bags /* Month-over-month change in total certified bags */ - LAG(f.total_bags, 1) OVER (ORDER BY f.report_date) AS mom_change_bags,
|
||||
ROUND(
|
||||
(
|
||||
f.total_bags::DOUBLE - LAG(f.total_bags, 1) OVER (ORDER BY f.report_date)::DOUBLE
|
||||
) / NULLIF(LAG(f.total_bags, 1) OVER (ORDER BY f.report_date)::DOUBLE, 0) * 100,
|
||||
2
|
||||
) AS mom_change_pct, /* Month-over-month percent change */
|
||||
ROUND(
|
||||
AVG(f.total_bags::DOUBLE) OVER (ORDER BY f.report_date ROWS BETWEEN 11 PRECEDING AND CURRENT ROW),
|
||||
0
|
||||
) AS avg_12m_bags, /* 12-month rolling average */
|
||||
f.source_file
|
||||
FROM foundation.fct_ice_warehouse_stocks_by_port AS f
|
||||
WHERE
|
||||
f.report_date BETWEEN @start_ds AND @end_ds
|
||||
)
|
||||
SELECT
|
||||
b.report_date,
|
||||
d.commodity_name,
|
||||
d.ice_stock_report_code,
|
||||
b.new_york_bags,
|
||||
b.new_orleans_bags,
|
||||
b.houston_bags,
|
||||
b.miami_bags,
|
||||
b.antwerp_bags,
|
||||
b.hamburg_bremen_bags,
|
||||
b.barcelona_bags,
|
||||
b.virginia_bags,
|
||||
b.total_bags,
|
||||
b.mom_change_bags,
|
||||
b.mom_change_pct,
|
||||
b.avg_12m_bags,
|
||||
b.source_file
|
||||
FROM base AS b
|
||||
CROSS JOIN foundation.dim_commodity AS d
|
||||
WHERE
|
||||
d.ice_stock_report_code = 'COFFEE-C'
|
||||
ORDER BY
|
||||
b.report_date
|
||||
@@ -1,106 +1,126 @@
|
||||
MODEL (
|
||||
name serving.commodity_metrics,
|
||||
kind INCREMENTAL_BY_TIME_RANGE (
|
||||
time_column ingest_date
|
||||
),
|
||||
start '2006-08-01',
|
||||
cron '@daily'
|
||||
);
|
||||
|
||||
-- CTE to calculate country-level derived metrics
|
||||
WITH country_metrics AS (
|
||||
SELECT
|
||||
commodity_code,
|
||||
commodity_name,
|
||||
country_code,
|
||||
country_name,
|
||||
market_year,
|
||||
ingest_date,
|
||||
Production,
|
||||
Imports,
|
||||
Exports,
|
||||
Total_Distribution,
|
||||
Ending_Stocks,
|
||||
-- Derived metrics per country, mirroring Python script
|
||||
(Production + Imports - Exports) AS Net_Supply,
|
||||
(Exports - Imports) AS Trade_Balance,
|
||||
(Production + Imports - Exports) - Total_Distribution AS Supply_Demand_Balance,
|
||||
-- Handle division by zero for Stock-to-Use Ratio
|
||||
(Ending_Stocks / NULLIF(Total_Distribution, 0)) * 100 AS Stock_to_Use_Ratio_pct,
|
||||
-- Calculate Production YoY percentage change using a window function
|
||||
(Production - LAG(Production, 1, 0) OVER (PARTITION BY commodity_code, country_code ORDER BY market_year, ingest_date)) / NULLIF(LAG(Production, 1, 0) OVER (PARTITION BY commodity_code, country_code ORDER BY market_year, ingest_date), 0) * 100 AS Production_YoY_pct
|
||||
FROM cleaned.psdalldata__commodity_pivoted
|
||||
),
|
||||
global_aggregates AS (
|
||||
SELECT
|
||||
commodity_code,
|
||||
commodity_name,
|
||||
NULL::TEXT AS country_code, -- Use NULL for global aggregates
|
||||
'Global' AS country_name,
|
||||
market_year,
|
||||
ingest_date,
|
||||
SUM(Production) AS Production,
|
||||
SUM(Imports) AS Imports,
|
||||
SUM(Exports) AS Exports,
|
||||
SUM(Total_Distribution) AS Total_Distribution,
|
||||
SUM(Ending_Stocks) AS Ending_Stocks
|
||||
FROM cleaned.psdalldata__commodity_pivoted
|
||||
GROUP BY
|
||||
commodity_code,
|
||||
commodity_name,
|
||||
market_year,
|
||||
ingest_date
|
||||
),
|
||||
-- CTE to calculate derived metrics for global aggregates
|
||||
global_metrics AS (
|
||||
SELECT
|
||||
commodity_code,
|
||||
commodity_name,
|
||||
country_code,
|
||||
country_name,
|
||||
market_year,
|
||||
ingest_date,
|
||||
Production,
|
||||
Imports,
|
||||
Exports,
|
||||
Total_Distribution,
|
||||
Ending_Stocks,
|
||||
(Production + Imports - Exports) AS Net_Supply,
|
||||
(Exports - Imports) AS Trade_Balance,
|
||||
(Production + Imports - Exports) - Total_Distribution AS Supply_Demand_Balance,
|
||||
(Ending_Stocks / NULLIF(Total_Distribution, 0)) * 100 AS Stock_to_Use_Ratio_pct,
|
||||
(Production - LAG(Production, 1, 0) OVER (PARTITION BY commodity_code ORDER BY market_year, ingest_date)) / NULLIF(LAG(Production, 1, 0) OVER (PARTITION BY commodity_code ORDER BY market_year, ingest_date), 0) * 100 AS Production_YoY_pct
|
||||
FROM global_aggregates
|
||||
)
|
||||
-- Combine country-level and global-level data into a single output
|
||||
SELECT
|
||||
commodity_code,
|
||||
commodity_name,
|
||||
country_code,
|
||||
country_name,
|
||||
market_year,
|
||||
ingest_date,
|
||||
Production,
|
||||
Imports,
|
||||
Exports,
|
||||
Total_Distribution,
|
||||
Ending_Stocks,
|
||||
Net_Supply,
|
||||
Trade_Balance,
|
||||
Supply_Demand_Balance,
|
||||
Stock_to_Use_Ratio_pct,
|
||||
Production_YoY_pct
|
||||
FROM (
|
||||
SELECT
|
||||
*
|
||||
FROM country_metrics
|
||||
UNION ALL
|
||||
SELECT
|
||||
*
|
||||
FROM global_metrics
|
||||
) AS combined_data
|
||||
ORDER BY
|
||||
commodity_name,
|
||||
country_name,
|
||||
market_year,
|
||||
ingest_date;
|
||||
MODEL (
|
||||
name serving.commodity_metrics,
|
||||
kind INCREMENTAL_BY_TIME_RANGE (
|
||||
time_column ingest_date
|
||||
),
|
||||
start '2006-08-01',
|
||||
cron '@daily'
|
||||
);
|
||||
|
||||
/* CTE to calculate country-level derived metrics */
|
||||
WITH country_metrics AS (
|
||||
SELECT
|
||||
commodity_code,
|
||||
commodity_name,
|
||||
country_code,
|
||||
country_name,
|
||||
market_year,
|
||||
ingest_date,
|
||||
Production,
|
||||
Imports,
|
||||
Exports,
|
||||
Total_Distribution,
|
||||
Ending_Stocks,
|
||||
(
|
||||
Production + Imports - Exports
|
||||
) AS Net_Supply, /* Derived metrics per country, mirroring Python script */
|
||||
(
|
||||
Exports - Imports
|
||||
) AS Trade_Balance,
|
||||
(
|
||||
Production + Imports - Exports
|
||||
) - Total_Distribution AS Supply_Demand_Balance,
|
||||
(
|
||||
Ending_Stocks / NULLIF(Total_Distribution, 0)
|
||||
) /* Handle division by zero for Stock-to-Use Ratio */ * 100 AS Stock_to_Use_Ratio_pct,
|
||||
(
|
||||
Production - LAG(Production, 1, 0) OVER (PARTITION BY commodity_code, country_code ORDER BY market_year, ingest_date)
|
||||
) /* Calculate Production YoY percentage change using a window function */ / NULLIF(
|
||||
LAG(Production, 1, 0) OVER (PARTITION BY commodity_code, country_code ORDER BY market_year, ingest_date),
|
||||
0
|
||||
) * 100 AS Production_YoY_pct
|
||||
FROM cleaned.psdalldata__commodity_pivoted
|
||||
), global_aggregates AS (
|
||||
SELECT
|
||||
commodity_code,
|
||||
commodity_name,
|
||||
NULL::TEXT AS country_code, /* Use NULL for global aggregates */
|
||||
'Global' AS country_name,
|
||||
market_year,
|
||||
ingest_date,
|
||||
SUM(Production) AS Production,
|
||||
SUM(Imports) AS Imports,
|
||||
SUM(Exports) AS Exports,
|
||||
SUM(Total_Distribution) AS Total_Distribution,
|
||||
SUM(Ending_Stocks) AS Ending_Stocks
|
||||
FROM cleaned.psdalldata__commodity_pivoted
|
||||
GROUP BY
|
||||
commodity_code,
|
||||
commodity_name,
|
||||
market_year,
|
||||
ingest_date
|
||||
), global_metrics /* CTE to calculate derived metrics for global aggregates */ AS (
|
||||
SELECT
|
||||
commodity_code,
|
||||
commodity_name,
|
||||
country_code,
|
||||
country_name,
|
||||
market_year,
|
||||
ingest_date,
|
||||
Production,
|
||||
Imports,
|
||||
Exports,
|
||||
Total_Distribution,
|
||||
Ending_Stocks,
|
||||
(
|
||||
Production + Imports - Exports
|
||||
) AS Net_Supply,
|
||||
(
|
||||
Exports - Imports
|
||||
) AS Trade_Balance,
|
||||
(
|
||||
Production + Imports - Exports
|
||||
) - Total_Distribution AS Supply_Demand_Balance,
|
||||
(
|
||||
Ending_Stocks / NULLIF(Total_Distribution, 0)
|
||||
) * 100 AS Stock_to_Use_Ratio_pct,
|
||||
(
|
||||
Production - LAG(Production, 1, 0) OVER (PARTITION BY commodity_code ORDER BY market_year, ingest_date)
|
||||
) / NULLIF(
|
||||
LAG(Production, 1, 0) OVER (PARTITION BY commodity_code ORDER BY market_year, ingest_date),
|
||||
0
|
||||
) * 100 AS Production_YoY_pct
|
||||
FROM global_aggregates
|
||||
)
|
||||
/* Combine country-level and global-level data into a single output */
|
||||
SELECT
|
||||
commodity_code,
|
||||
commodity_name,
|
||||
country_code,
|
||||
country_name,
|
||||
market_year,
|
||||
ingest_date,
|
||||
Production,
|
||||
Imports,
|
||||
Exports,
|
||||
Total_Distribution,
|
||||
Ending_Stocks,
|
||||
Net_Supply,
|
||||
Trade_Balance,
|
||||
Supply_Demand_Balance,
|
||||
Stock_to_Use_Ratio_pct,
|
||||
Production_YoY_pct
|
||||
FROM (
|
||||
SELECT
|
||||
*
|
||||
FROM country_metrics
|
||||
UNION ALL
|
||||
SELECT
|
||||
*
|
||||
FROM global_metrics
|
||||
) AS combined_data
|
||||
ORDER BY
|
||||
commodity_name,
|
||||
country_name,
|
||||
market_year,
|
||||
ingest_date
|
||||
@@ -1,41 +1,32 @@
|
||||
-- Serving mart: COT positioning for Coffee C futures, analytics-ready.
|
||||
--
|
||||
-- Joins foundation.fct_cot_positioning with foundation.dim_commodity so
|
||||
-- the coffee filter is driven by the dimension (not a hardcoded CFTC code).
|
||||
-- Adds derived analytics used by the dashboard and API:
|
||||
-- - Normalized positioning (% of open interest)
|
||||
-- - Long/short ratio
|
||||
-- - Week-over-week momentum
|
||||
-- - COT Index over 26-week and 52-week trailing windows (0=bearish, 100=bullish)
|
||||
--
|
||||
-- Grain: one row per report_date for Coffee C futures.
|
||||
-- Latest revision per date: MAX(ingest_date) used to deduplicate CFTC corrections.
|
||||
|
||||
/* Serving mart: COT positioning for Coffee C futures, analytics-ready. */ /* Joins foundation.fct_cot_positioning with foundation.dim_commodity so */ /* the coffee filter is driven by the dimension (not a hardcoded CFTC code). */ /* Adds derived analytics used by the dashboard and API: */ /* - Normalized positioning (% of open interest) */ /* - Long/short ratio */ /* - Week-over-week momentum */ /* - COT Index over 26-week and 52-week trailing windows (0=bearish, 100=bullish) */ /* Grain: one row per report_date for Coffee C futures. */ /* Latest revision per date: MAX(ingest_date) used to deduplicate CFTC corrections. */
|
||||
MODEL (
|
||||
name serving.cot_positioning,
|
||||
kind INCREMENTAL_BY_TIME_RANGE (
|
||||
time_column report_date
|
||||
),
|
||||
grain (report_date),
|
||||
grain (
|
||||
report_date
|
||||
),
|
||||
start '2006-06-13',
|
||||
cron '@daily'
|
||||
);
|
||||
|
||||
WITH latest_revision AS (
|
||||
-- Pick the most recently ingested row when CFTC issues corrections
|
||||
SELECT f.*
|
||||
FROM foundation.fct_cot_positioning f
|
||||
INNER JOIN foundation.dim_commodity d
|
||||
/* Pick the most recently ingested row when CFTC issues corrections */
|
||||
SELECT
|
||||
f.*
|
||||
FROM foundation.fct_cot_positioning AS f
|
||||
INNER JOIN foundation.dim_commodity AS d
|
||||
ON f.cftc_commodity_code = d.cftc_commodity_code
|
||||
WHERE d.commodity_name = 'Coffee, Green'
|
||||
WHERE
|
||||
d.commodity_name = 'Coffee, Green'
|
||||
AND f.report_date BETWEEN @start_ds AND @end_ds
|
||||
QUALIFY ROW_NUMBER() OVER (
|
||||
PARTITION BY f.report_date, f.cftc_contract_market_code
|
||||
ORDER BY f.ingest_date DESC
|
||||
) = 1
|
||||
),
|
||||
|
||||
with_derived AS (
|
||||
QUALIFY
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY f.report_date, f.cftc_contract_market_code
|
||||
ORDER BY f.ingest_date DESC
|
||||
) = 1
|
||||
), with_derived AS (
|
||||
SELECT
|
||||
report_date,
|
||||
market_and_exchange_name,
|
||||
@@ -43,9 +34,7 @@ with_derived AS (
|
||||
cftc_contract_market_code,
|
||||
contract_units,
|
||||
ingest_date,
|
||||
|
||||
-- Absolute positions (contracts)
|
||||
open_interest,
|
||||
open_interest, /* Absolute positions (contracts) */
|
||||
managed_money_long,
|
||||
managed_money_short,
|
||||
managed_money_spread,
|
||||
@@ -64,77 +53,52 @@ with_derived AS (
|
||||
nonreportable_long,
|
||||
nonreportable_short,
|
||||
nonreportable_net,
|
||||
|
||||
-- Normalized: managed money net as % of open interest
|
||||
-- Removes size effects and makes cross-period comparison meaningful
|
||||
round(
|
||||
managed_money_net::float / NULLIF(open_interest, 0) * 100,
|
||||
2
|
||||
) AS managed_money_net_pct_of_oi,
|
||||
|
||||
-- Long/short ratio: >1 = more bulls than bears in managed money
|
||||
round(
|
||||
managed_money_long::float / NULLIF(managed_money_short, 0),
|
||||
3
|
||||
) AS managed_money_long_short_ratio,
|
||||
|
||||
-- Weekly changes
|
||||
change_open_interest,
|
||||
ROUND(managed_money_net::REAL / NULLIF(open_interest, 0) * 100, 2) AS managed_money_net_pct_of_oi, /* Normalized: managed money net as % of open interest */ /* Removes size effects and makes cross-period comparison meaningful */
|
||||
ROUND(managed_money_long::REAL / NULLIF(managed_money_short, 0), 3) AS managed_money_long_short_ratio, /* Long/short ratio: >1 = more bulls than bears in managed money */
|
||||
change_open_interest, /* Weekly changes */
|
||||
change_managed_money_long,
|
||||
change_managed_money_short,
|
||||
change_managed_money_net,
|
||||
change_prod_merc_long,
|
||||
change_prod_merc_short,
|
||||
|
||||
-- Week-over-week momentum in managed money net (via LAG)
|
||||
managed_money_net - LAG(managed_money_net, 1) OVER (
|
||||
ORDER BY report_date
|
||||
) AS managed_money_net_wow,
|
||||
|
||||
-- Concentration
|
||||
concentration_top4_long_pct,
|
||||
managed_money_net /* Week-over-week momentum in managed money net (via LAG) */ - LAG(managed_money_net, 1) OVER (ORDER BY report_date) AS managed_money_net_wow,
|
||||
concentration_top4_long_pct, /* Concentration */
|
||||
concentration_top4_short_pct,
|
||||
concentration_top8_long_pct,
|
||||
concentration_top8_short_pct,
|
||||
|
||||
-- Trader counts
|
||||
traders_total,
|
||||
traders_total, /* Trader counts */
|
||||
traders_managed_money_long,
|
||||
traders_managed_money_short,
|
||||
traders_managed_money_spread,
|
||||
|
||||
-- COT Index (26-week): where is current net vs. trailing 26 weeks?
|
||||
-- 0 = most bearish extreme, 100 = most bullish extreme
|
||||
-- Industry-standard sentiment gauge (equivalent to RSI for positioning)
|
||||
CASE
|
||||
WHEN MAX(managed_money_net) OVER w26 = MIN(managed_money_net) OVER w26
|
||||
THEN 50.0
|
||||
ELSE round(
|
||||
(managed_money_net - MIN(managed_money_net) OVER w26)::float
|
||||
/ (MAX(managed_money_net) OVER w26 - MIN(managed_money_net) OVER w26)
|
||||
* 100,
|
||||
THEN 50.0
|
||||
ELSE ROUND(
|
||||
(
|
||||
managed_money_net - MIN(managed_money_net) OVER w26
|
||||
)::REAL / (
|
||||
MAX(managed_money_net) OVER w26 - MIN(managed_money_net) OVER w26
|
||||
) * 100,
|
||||
1
|
||||
)
|
||||
END AS cot_index_26w,
|
||||
|
||||
-- COT Index (52-week): longer-term positioning context
|
||||
END AS cot_index_26w, /* COT Index (26-week): where is current net vs. trailing 26 weeks? */ /* 0 = most bearish extreme, 100 = most bullish extreme */ /* Industry-standard sentiment gauge (equivalent to RSI for positioning) */
|
||||
CASE
|
||||
WHEN MAX(managed_money_net) OVER w52 = MIN(managed_money_net) OVER w52
|
||||
THEN 50.0
|
||||
ELSE round(
|
||||
(managed_money_net - MIN(managed_money_net) OVER w52)::float
|
||||
/ (MAX(managed_money_net) OVER w52 - MIN(managed_money_net) OVER w52)
|
||||
* 100,
|
||||
THEN 50.0
|
||||
ELSE ROUND(
|
||||
(
|
||||
managed_money_net - MIN(managed_money_net) OVER w52
|
||||
)::REAL / (
|
||||
MAX(managed_money_net) OVER w52 - MIN(managed_money_net) OVER w52
|
||||
) * 100,
|
||||
1
|
||||
)
|
||||
END AS cot_index_52w
|
||||
|
||||
END AS cot_index_52w /* COT Index (52-week): longer-term positioning context */
|
||||
FROM latest_revision
|
||||
WINDOW
|
||||
w26 AS (ORDER BY report_date ROWS BETWEEN 25 PRECEDING AND CURRENT ROW),
|
||||
w52 AS (ORDER BY report_date ROWS BETWEEN 51 PRECEDING AND CURRENT ROW)
|
||||
WINDOW w26 AS (ORDER BY report_date ROWS BETWEEN 25 PRECEDING AND CURRENT ROW), w52 AS (ORDER BY report_date ROWS BETWEEN 51 PRECEDING AND CURRENT ROW)
|
||||
)
|
||||
|
||||
SELECT *
|
||||
SELECT
|
||||
*
|
||||
FROM with_derived
|
||||
ORDER BY report_date
|
||||
ORDER BY
|
||||
report_date
|
||||
Reference in New Issue
Block a user