← All examples
63

Canonical Grid model

Economics - Macro forecast holdout starter

Synthetic quarterly output index, fixed training window and eight-quarter holdout; no real-time forecasting claim.

Scale
Small
Source
economics-macro-starter.grid
Length
81 lines
Collection
Advanced language
Level
Intermediate
Runtime
Portable
Version
1.0.0
economics-macro-starter.grid
Get Grid
MODEL "Economics - Macro forecast holdout starter"
DESCRIPTION "Synthetic quarterly output index, fixed training window and eight-quarter holdout; no real-time forecasting claim."
VERSION "1.0.0"
AUTHOR "Grid"
TAGS "economics", "forecast", "starter"

input ArWeight = 1
  VALIDATE BETWEEN 0 AND 1 MESSAGE "Choose an AR forecast weight between 0 and 1."
input OriginShock = 0
  VALIDATE BETWEEN -5 AND 5 MESSAGE "Choose an origin growth shock between -5 and 5 percentage points."

# Frozen synthetic data: 32 quarter-start observations from 2017 Q1 to 2024 Q4.
# Levels start at 100 and compound a deterministic trend/seasonal growth process.
# A holdout-only disturbance makes the benchmark comparison informative.
# This currently uses the existing economic_series representation.
OutputSeries IS economic_series = PARSE_JSON("{\"series\":{\"frequency_short\":\"Q\",\"title\":\"Synthetic quarterly output index\",\"units\":\"index, first period = 100\",\"source\":\"Deterministic teaching fixture; no observed economy\"},\"observations\":{\"observations\":[{\"date\":\"2017-01-01\",\"value\":100},{\"date\":\"2017-04-01\",\"value\":100.851776695},{\"date\":\"2017-07-01\",\"value\":101.633377965},{\"date\":\"2017-10-01\",\"value\":102.270392292},{\"date\":\"2018-01-01\",\"value\":102.807311852},{\"date\":\"2018-04-01\",\"value\":103.319521839},{\"date\":\"2018-07-01\",\"value\":103.836119448},{\"date\":\"2018-10-01\",\"value\":104.353455194},{\"date\":\"2019-01-01\",\"value\":104.901310833},{\"date\":\"2019-04-01\",\"value\":105.558807803},{\"date\":\"2019-07-01\",\"value\":106.376888563},{\"date\":\"2019-10-01\",\"value\":107.282982109},{\"date\":\"2020-01-01\",\"value\":108.087604475},{\"date\":\"2020-04-01\",\"value\":108.62612211},{\"date\":\"2020-07-01\",\"value\":108.924843946},{\"date\":\"2020-10-01\",\"value\":109.222452004},{\"date\":\"2021-01-01\",\"value\":109.795869877},{\"date\":\"2021-04-01\",\"value\":110.731085509},{\"date\":\"2021-07-01\",\"value\":111.838396364},{\"date\":\"2021-10-01\",\"value\":112.791009761},{\"date\":\"2022-01-01\",\"value\":113.383162562},{\"date\":\"2022-04-01\",\"value\":113.692951786},{\"date\":\"2022-07-01\",\"value\":114.005607403},{\"date\":\"2022-10-01\",\"value\":114.573609908},{\"date\":\"2023-01-01\",\"value\":114.516323103},{\"date\":\"2023-04-01\",\"value\":114.91916484},{\"date\":\"2023-07-01\",\"value\":115.924707532},{\"date\":\"2023-10-01\",\"value\":116.883145998},{\"date\":\"2024-01-01\",\"value\":117.146133077},{\"date\":\"2024-04-01\",\"value\":117.846928545},{\"date\":\"2024-07-01\",\"value\":118.907550902},{\"date\":\"2024-10-01\",\"value\":119.737791134}]}}")
GrowthSeries = TS.PCT_CHANGE(OutputSeries, 1)
GrowthRecords = GrowthSeries.observations.observations

# Fixed split: 23 growth observations through 2022 Q4, eight held out in 2023-2024.
# The first level has no prior-quarter growth and is explicitly excluded.
GrowthValues = MAP(SEQUENCE(1, 31), (position) => INDEX(GrowthRecords, 1, position + 1).value)
TrainingCount = 23
HoldoutCount = 8
TrainingGrowth = MAP(SEQUENCE(1, TrainingCount), (position) => INDEX(GrowthValues, 1, position))
TrainingLag = MAP(SEQUENCE(1, TrainingCount - 1), (position) => INDEX(TrainingGrowth, 1, position))
TrainingLead = MAP(SEQUENCE(1, TrainingCount - 1), (position) => INDEX(TrainingGrowth, 1, position + 1))
HoldoutGrowth = MAP(SEQUENCE(1, HoldoutCount), (position) => INDEX(GrowthValues, 1, TrainingCount + position))
TrainingMean = AVERAGE(TrainingGrowth)
Autoregression = REGRESS(TrainingLead, TRANSPOSE(TrainingLag), "classical", 0, TRUE, 0.95, {termNames: ["Constant", "Previous quarter growth"]})
ArIntercept = INDEX(Autoregression.coefficients, 1, 1)
ArSlope = INDEX(Autoregression.coefficients, 1, 2)

# Forecast all eight horizons from the training endpoint; never feed back holdout actuals.
RawForecasts = SCAN(INDEX(TrainingGrowth, 1, TrainingCount) + OriginShock, SEQUENCE(1, HoldoutCount), (previous, horizon) => ArIntercept + ArSlope * previous)
BlendedForecasts = MAP(RawForecasts, (forecast) => ArWeight * forecast + (1 - ArWeight) * TrainingMean)
ForecastErrors = MAP(BlendedForecasts, HoldoutGrowth, (forecast, actual) => forecast - actual)
BenchmarkErrors = MAP(HoldoutGrowth, (actual) => TrainingMean - actual)

output CalendarGrowth = GrowthSeries
output AnnualChange = TS.YOY(OutputSeries)
output TrainingWindow = "2017 Q2 to 2022 Q4: 23 growth observations, 22 AR pairs"
output HoldoutWindow = "2023 Q1 to 2024 Q4: eight untouched growth observations"
output FitComparison = REGRESS.TABLE(Autoregression)
output PointForecasts = BlendedForecasts
output HoldoutActuals = HoldoutGrowth
output MeanBenchmark = TrainingMean
output ForecastRMSE = SQRT(AVERAGE(MAP(ForecastErrors, (error) => error^2)))
output BenchmarkRMSE = SQRT(AVERAGE(MAP(BenchmarkErrors, (error) => error^2)))
output ForecastMAE = AVERAGE(MAP(ForecastErrors, (error) => ABS(error)))
output BenchmarkMAE = AVERAGE(MAP(BenchmarkErrors, (error) => ABS(error)))
output CalendarCountCheck = ASSERT_IDENTITY(COUNT(GrowthValues), 31)
output SplitCountCheck = ASSERT_IDENTITY(COLUMNS(TrainingGrowth) + COLUMNS(HoldoutGrowth), COLUMNS(GrowthValues))
output Interpretation = "Growth and errors are quarterly percentage points, not annualized. Weight and shock are prespecified scenario assumptions; choosing them after inspecting this holdout would invalidate it as an untouched evaluation. Coefficient confidence intervals are not forecast intervals. This frozen synthetic exercise does not model data-release delays or revisions. Retrospective HP/Hamilton filters are deliberately outside the forecast path."

A1 = "Macro holdout"
A3 = "Training"
B3 = "2017 Q2"
C3 = "2022 Q4"
D3 = "23 quarters"
A4 = "Holdout"
B4 = "2023 Q1"
C4 = "2024 Q4"
D4 = "8 quarters"
B5 = "Scenario"
C5 = "Benchmark"
A6 = "RMSE"
B6 = ForecastRMSE
C6 = BenchmarkRMSE
A7 = "MAE"
B7 = ForecastMAE
C7 = BenchmarkMAE
A9 = "Forecasts"
B9 = PointForecasts
A10 = "Actuals"
B10 = HoldoutActuals
A12 = "Calendar check"
B12 = CalendarCountCheck
A13 = "Split check"
B13 = SplitCountCheck
END MODEL