/// Embedded historical market data derived from Robert Shiller's CAPE dataset. /// /// Source: https://shillerdata.com/ (ie_data.xls, "Data" tab saved as CSV) /// /// To update: /// 1. Download ie_data.xls from https://shillerdata.com/ /// 2. Open in LibreOffice Calc, select the "Data" tab /// 3. File -> Save As -> CSV (ie_data.csv) /// 4. Replace src/data/ie_data.csv with the new file /// 5. Rebuild - build/gen_shiller.zig regenerates the data automatically /// 6. Bump `ie_data_last_updated` below to today's date. /// /// All returns are nominal, expressed as decimals (0.12 = 12%). const std = @import("std"); const Date = @import("../Date.zig"); const generated = @import("shiller_generated"); pub const ShillerYear = @import("shiller_year").ShillerYear; /// Last time `ie_data.csv` was refreshed. Bump this whenever you /// replace the CSV - drives the annual staleness nag in /// `src/data/staleness.zig` (nags on stderr from April 1 each year /// until refreshed). pub const ie_data_last_updated: Date = Date.fromYmd(2026, 4, 27); /// Annual returns from the Shiller dataset, generated at build time from ie_data.csv. pub const annual_returns: []const ShillerYear = &generated.data; /// Number of available years of historical data. pub const year_count: usize = annual_returns.len; /// First year with complete data. pub const first_year: u16 = if (annual_returns.len > 0) annual_returns[0].year else 0; /// Last year with complete data. pub const last_year: u16 = if (annual_returns.len > 0) annual_returns[annual_returns.len - 1].year else 0; /// Convert a nominal return to a real (inflation-adjusted) return. pub fn realReturn(nominal: f64, inflation: f64) f64 { return (1.0 + nominal) / (1.0 + inflation) - 1.0; } /// Return the maximum number of complete simulation cycles for a given horizon. /// A cycle starting in year Y needs data through year Y + horizon - 1. pub fn maxCycles(horizon: u16) usize { if (annual_returns.len == 0) return 0; const span = last_year - first_year + 1; if (horizon > span) return 0; return span - horizon + 1; } // --- Tests --- test "annual returns are populated" { try std.testing.expect(annual_returns.len >= 150); try std.testing.expectEqual(@as(u16, 1871), first_year); try std.testing.expect(last_year >= 2024); } test "spot check 2008 crash" { for (annual_returns) |yr| { if (yr.year == 2008) { try std.testing.expect(yr.sp500_total_return < -0.30); try std.testing.expect(yr.sp500_total_return > -0.50); return; } } return error.YearNotFound; } test "spot check 1929 crash" { for (annual_returns) |yr| { if (yr.year == 1929) { try std.testing.expect(yr.sp500_total_return < -0.05); return; } } return error.YearNotFound; } test "realReturn calculation" { const real = realReturn(0.10, 0.03); try std.testing.expectApproxEqAbs(0.06796, real, 0.001); } test "maxCycles" { const cycles = maxCycles(30); try std.testing.expect(cycles >= 120); try std.testing.expect(cycles <= 130); try std.testing.expectEqual(@as(usize, 0), maxCycles(200)); } test "spot check known annual total returns" { const checks = [_]struct { year: u16, min: f64, max: f64 }{ .{ .year = 1931, .min = -0.55, .max = -0.25 }, .{ .year = 1933, .min = 0.30, .max = 0.80 }, .{ .year = 1974, .min = -0.40, .max = -0.05 }, .{ .year = 2008, .min = -0.50, .max = -0.25 }, .{ .year = 2009, .min = 0.15, .max = 0.50 }, .{ .year = 2021, .min = 0.10, .max = 0.45 }, }; for (checks) |chk| { for (annual_returns) |yr| { if (yr.year == chk.year) { if (yr.sp500_total_return < chk.min or yr.sp500_total_return > chk.max) { return error.TestExpectedEqual; } break; } } } } test "bond returns are reasonable" { for (annual_returns) |yr| { if (yr.bond_total_return < 0.0 or yr.bond_total_return > 0.16) return error.TestExpectedEqual; } } test "CPI inflation is reasonable" { for (annual_returns) |yr| { if (yr.cpi_inflation < -0.20 or yr.cpi_inflation > 0.25) return error.TestExpectedEqual; } }