timeprice 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -0
  3. data/README.md +73 -2
  4. data/data/cpi/au.json +419 -0
  5. data/data/cpi/br.json +529 -0
  6. data/data/cpi/ca.json +1501 -0
  7. data/data/cpi/ch.json +549 -0
  8. data/data/cpi/cn.json +487 -0
  9. data/data/cpi/cz.json +500 -0
  10. data/data/cpi/eu.json +1 -1
  11. data/data/cpi/hk.json +351 -0
  12. data/data/cpi/hu.json +537 -0
  13. data/data/cpi/id.json +550 -0
  14. data/data/cpi/il.json +549 -0
  15. data/data/cpi/in.json +549 -0
  16. data/data/cpi/jp.json +2 -2
  17. data/data/cpi/kr.json +550 -0
  18. data/data/cpi/mx.json +550 -0
  19. data/data/cpi/my.json +429 -0
  20. data/data/cpi/no.json +549 -0
  21. data/data/cpi/nz.json +94 -0
  22. data/data/cpi/ph.json +309 -0
  23. data/data/cpi/pl.json +539 -0
  24. data/data/cpi/ru.json +487 -0
  25. data/data/cpi/se.json +549 -0
  26. data/data/cpi/sg.json +369 -0
  27. data/data/cpi/th.json +309 -0
  28. data/data/cpi/tr.json +549 -0
  29. data/data/cpi/uk.json +1 -1
  30. data/data/cpi/us.json +1007 -5
  31. data/data/cpi/vn.json +32 -32
  32. data/data/cpi/za.json +549 -0
  33. data/data/fx/usd/1999.json +5982 -262
  34. data/data/fx/usd/2000.json +6999 -258
  35. data/data/fx/usd/2001.json +7142 -257
  36. data/data/fx/usd/2002.json +7170 -258
  37. data/data/fx/usd/2003.json +7170 -258
  38. data/data/fx/usd/2004.json +7282 -262
  39. data/data/fx/usd/2005.json +7226 -260
  40. data/data/fx/usd/2006.json +7170 -258
  41. data/data/fx/usd/2007.json +7169 -258
  42. data/data/fx/usd/2008.json +7184 -259
  43. data/data/fx/usd/2009.json +6941 -259
  44. data/data/fx/usd/2010.json +6995 -261
  45. data/data/fx/usd/2011.json +6968 -260
  46. data/data/fx/usd/2012.json +6941 -259
  47. data/data/fx/usd/2013.json +6914 -258
  48. data/data/fx/usd/2014.json +6914 -258
  49. data/data/fx/usd/2015.json +6941 -259
  50. data/data/fx/usd/2016.json +6968 -260
  51. data/data/fx/usd/2017.json +6914 -258
  52. data/data/fx/usd/2018.json +7148 -258
  53. data/data/fx/usd/2019.json +7170 -258
  54. data/data/fx/usd/2020.json +7226 -260
  55. data/data/fx/usd/2021.json +7254 -261
  56. data/data/fx/usd/2022.json +7226 -260
  57. data/data/fx/usd/2023.json +7170 -258
  58. data/data/fx/usd/2024.json +7198 -259
  59. data/data/fx/usd/2025.json +7170 -258
  60. data/data/fx/usd/2026.json +2561 -92
  61. data/data/fx/usd/_annual.json +46 -1
  62. data/data/manifest.json +565 -2
  63. data/lib/timeprice/cli/presenters/compare.rb +44 -8
  64. data/lib/timeprice/cli.rb +24 -7
  65. data/lib/timeprice/compare/series.rb +120 -0
  66. data/lib/timeprice/compare.rb +108 -15
  67. data/lib/timeprice/cpi_lookup.rb +14 -8
  68. data/lib/timeprice/data_loader.rb +8 -17
  69. data/lib/timeprice/date.rb +62 -0
  70. data/lib/timeprice/exchange.rb +49 -23
  71. data/lib/timeprice/forecast/cagr.rb +96 -0
  72. data/lib/timeprice/forecast/cpi_forecaster.rb +90 -0
  73. data/lib/timeprice/forecast/fx_forecaster.rb +173 -0
  74. data/lib/timeprice/forecast.rb +21 -0
  75. data/lib/timeprice/inflation.rb +12 -4
  76. data/lib/timeprice/metadata.rb +121 -0
  77. data/lib/timeprice/metadata_snapshot.rb +23 -0
  78. data/lib/timeprice/point.rb +11 -3
  79. data/lib/timeprice/schema.rb +78 -0
  80. data/lib/timeprice/sources.rb +1 -1
  81. data/lib/timeprice/supported.rb +16 -0
  82. data/lib/timeprice/version.rb +1 -1
  83. data/lib/timeprice.rb +48 -3
  84. metadata +49 -2
data/data/manifest.json CHANGED
@@ -1,25 +1,483 @@
1
1
  {
2
2
  "countries": [
3
+ {
4
+ "code": "AU",
5
+ "cpi_file": "cpi/au.json",
6
+ "cpi_ranges": {
7
+ "annual": {
8
+ "max": "2024",
9
+ "min": "1960"
10
+ },
11
+ "quarterly": {
12
+ "max": "2026-Q1",
13
+ "min": "1948-Q3"
14
+ }
15
+ },
16
+ "currency": "AUD",
17
+ "granularities": [
18
+ "quarterly",
19
+ "annual"
20
+ ]
21
+ },
22
+ {
23
+ "code": "BR",
24
+ "cpi_file": "cpi/br.json",
25
+ "cpi_ranges": {
26
+ "annual": {
27
+ "max": "2025",
28
+ "min": "1980"
29
+ },
30
+ "monthly": {
31
+ "max": "2026-03",
32
+ "min": "1990-01"
33
+ }
34
+ },
35
+ "currency": "BRL",
36
+ "granularities": [
37
+ "monthly",
38
+ "annual"
39
+ ]
40
+ },
41
+ {
42
+ "code": "CA",
43
+ "cpi_file": "cpi/ca.json",
44
+ "cpi_ranges": {
45
+ "annual": {
46
+ "max": "2025",
47
+ "min": "1914"
48
+ },
49
+ "monthly": {
50
+ "max": "2026-03",
51
+ "min": "1914-01"
52
+ }
53
+ },
54
+ "currency": "CAD",
55
+ "granularities": [
56
+ "monthly",
57
+ "annual"
58
+ ]
59
+ },
60
+ {
61
+ "code": "CH",
62
+ "cpi_file": "cpi/ch.json",
63
+ "cpi_ranges": {
64
+ "annual": {
65
+ "max": "2025",
66
+ "min": "1960"
67
+ },
68
+ "monthly": {
69
+ "max": "2026-03",
70
+ "min": "1990-01"
71
+ }
72
+ },
73
+ "currency": "CHF",
74
+ "granularities": [
75
+ "monthly",
76
+ "annual"
77
+ ]
78
+ },
79
+ {
80
+ "code": "CN",
81
+ "cpi_file": "cpi/cn.json",
82
+ "cpi_ranges": {
83
+ "annual": {
84
+ "max": "2025",
85
+ "min": "1986"
86
+ },
87
+ "monthly": {
88
+ "max": "2026-03",
89
+ "min": "1993-01"
90
+ }
91
+ },
92
+ "currency": "CNY",
93
+ "granularities": [
94
+ "monthly",
95
+ "annual"
96
+ ]
97
+ },
98
+ {
99
+ "code": "CZ",
100
+ "cpi_file": "cpi/cz.json",
101
+ "cpi_ranges": {
102
+ "annual": {
103
+ "max": "2025",
104
+ "min": "1991"
105
+ },
106
+ "monthly": {
107
+ "max": "2026-03",
108
+ "min": "1991-01"
109
+ }
110
+ },
111
+ "currency": "CZK",
112
+ "granularities": [
113
+ "monthly",
114
+ "annual"
115
+ ]
116
+ },
3
117
  {
4
118
  "code": "EU",
5
119
  "cpi_file": "cpi/eu.json",
120
+ "cpi_ranges": {
121
+ "annual": {
122
+ "max": "2025",
123
+ "min": "1996"
124
+ },
125
+ "monthly": {
126
+ "max": "2025-12",
127
+ "min": "1996-01"
128
+ }
129
+ },
6
130
  "currency": "EUR",
7
131
  "granularities": [
8
132
  "monthly",
9
133
  "annual"
10
134
  ]
11
135
  },
136
+ {
137
+ "code": "HK",
138
+ "cpi_file": "cpi/hk.json",
139
+ "cpi_ranges": {
140
+ "annual": {
141
+ "max": "2025",
142
+ "min": "1981"
143
+ },
144
+ "monthly": {
145
+ "max": "2026-03",
146
+ "min": "2004-10"
147
+ }
148
+ },
149
+ "currency": "HKD",
150
+ "granularities": [
151
+ "monthly",
152
+ "annual"
153
+ ]
154
+ },
155
+ {
156
+ "code": "HU",
157
+ "cpi_file": "cpi/hu.json",
158
+ "cpi_ranges": {
159
+ "annual": {
160
+ "max": "2025",
161
+ "min": "1972"
162
+ },
163
+ "monthly": {
164
+ "max": "2026-03",
165
+ "min": "1990-01"
166
+ }
167
+ },
168
+ "currency": "HUF",
169
+ "granularities": [
170
+ "monthly",
171
+ "annual"
172
+ ]
173
+ },
174
+ {
175
+ "code": "ID",
176
+ "cpi_file": "cpi/id.json",
177
+ "cpi_ranges": {
178
+ "annual": {
179
+ "max": "2025",
180
+ "min": "1960"
181
+ },
182
+ "monthly": {
183
+ "max": "2026-04",
184
+ "min": "1990-01"
185
+ }
186
+ },
187
+ "currency": "IDR",
188
+ "granularities": [
189
+ "monthly",
190
+ "annual"
191
+ ]
192
+ },
193
+ {
194
+ "code": "IL",
195
+ "cpi_file": "cpi/il.json",
196
+ "cpi_ranges": {
197
+ "annual": {
198
+ "max": "2025",
199
+ "min": "1960"
200
+ },
201
+ "monthly": {
202
+ "max": "2026-03",
203
+ "min": "1990-01"
204
+ }
205
+ },
206
+ "currency": "ILS",
207
+ "granularities": [
208
+ "monthly",
209
+ "annual"
210
+ ]
211
+ },
212
+ {
213
+ "code": "IN",
214
+ "cpi_file": "cpi/in.json",
215
+ "cpi_ranges": {
216
+ "annual": {
217
+ "max": "2025",
218
+ "min": "1960"
219
+ },
220
+ "monthly": {
221
+ "max": "2026-03",
222
+ "min": "1990-01"
223
+ }
224
+ },
225
+ "currency": "INR",
226
+ "granularities": [
227
+ "monthly",
228
+ "annual"
229
+ ]
230
+ },
12
231
  {
13
232
  "code": "JP",
14
233
  "cpi_file": "cpi/jp.json",
234
+ "cpi_ranges": {
235
+ "annual": {
236
+ "max": "2024",
237
+ "min": "1960"
238
+ }
239
+ },
15
240
  "currency": "JPY",
16
241
  "granularities": [
17
242
  "annual"
18
243
  ]
19
244
  },
245
+ {
246
+ "code": "KR",
247
+ "cpi_file": "cpi/kr.json",
248
+ "cpi_ranges": {
249
+ "annual": {
250
+ "max": "2025",
251
+ "min": "1960"
252
+ },
253
+ "monthly": {
254
+ "max": "2026-04",
255
+ "min": "1990-01"
256
+ }
257
+ },
258
+ "currency": "KRW",
259
+ "granularities": [
260
+ "monthly",
261
+ "annual"
262
+ ]
263
+ },
264
+ {
265
+ "code": "MX",
266
+ "cpi_file": "cpi/mx.json",
267
+ "cpi_ranges": {
268
+ "annual": {
269
+ "max": "2025",
270
+ "min": "1960"
271
+ },
272
+ "monthly": {
273
+ "max": "2026-04",
274
+ "min": "1990-01"
275
+ }
276
+ },
277
+ "currency": "MXN",
278
+ "granularities": [
279
+ "monthly",
280
+ "annual"
281
+ ]
282
+ },
283
+ {
284
+ "code": "MY",
285
+ "cpi_file": "cpi/my.json",
286
+ "cpi_ranges": {
287
+ "annual": {
288
+ "max": "2025",
289
+ "min": "1960"
290
+ },
291
+ "monthly": {
292
+ "max": "2026-03",
293
+ "min": "2000-01"
294
+ }
295
+ },
296
+ "currency": "MYR",
297
+ "granularities": [
298
+ "monthly",
299
+ "annual"
300
+ ]
301
+ },
302
+ {
303
+ "code": "NO",
304
+ "cpi_file": "cpi/no.json",
305
+ "cpi_ranges": {
306
+ "annual": {
307
+ "max": "2025",
308
+ "min": "1960"
309
+ },
310
+ "monthly": {
311
+ "max": "2026-03",
312
+ "min": "1990-01"
313
+ }
314
+ },
315
+ "currency": "NOK",
316
+ "granularities": [
317
+ "monthly",
318
+ "annual"
319
+ ]
320
+ },
321
+ {
322
+ "code": "NZ",
323
+ "cpi_file": "cpi/nz.json",
324
+ "cpi_ranges": {
325
+ "annual": {
326
+ "max": "2024",
327
+ "min": "1960"
328
+ }
329
+ },
330
+ "currency": "NZD",
331
+ "granularities": [
332
+ "annual"
333
+ ]
334
+ },
335
+ {
336
+ "code": "PH",
337
+ "cpi_file": "cpi/ph.json",
338
+ "cpi_ranges": {
339
+ "annual": {
340
+ "max": "2025",
341
+ "min": "1960"
342
+ },
343
+ "monthly": {
344
+ "max": "2026-03",
345
+ "min": "2010-01"
346
+ }
347
+ },
348
+ "currency": "PHP",
349
+ "granularities": [
350
+ "monthly",
351
+ "annual"
352
+ ]
353
+ },
354
+ {
355
+ "code": "PL",
356
+ "cpi_file": "cpi/pl.json",
357
+ "cpi_ranges": {
358
+ "annual": {
359
+ "max": "2025",
360
+ "min": "1970"
361
+ },
362
+ "monthly": {
363
+ "max": "2026-03",
364
+ "min": "1990-01"
365
+ }
366
+ },
367
+ "currency": "PLN",
368
+ "granularities": [
369
+ "monthly",
370
+ "annual"
371
+ ]
372
+ },
373
+ {
374
+ "code": "RU",
375
+ "cpi_file": "cpi/ru.json",
376
+ "cpi_ranges": {
377
+ "annual": {
378
+ "max": "2025",
379
+ "min": "1992"
380
+ },
381
+ "monthly": {
382
+ "max": "2026-03",
383
+ "min": "1992-01"
384
+ }
385
+ },
386
+ "currency": "RUB",
387
+ "granularities": [
388
+ "monthly",
389
+ "annual"
390
+ ]
391
+ },
392
+ {
393
+ "code": "SE",
394
+ "cpi_file": "cpi/se.json",
395
+ "cpi_ranges": {
396
+ "annual": {
397
+ "max": "2025",
398
+ "min": "1960"
399
+ },
400
+ "monthly": {
401
+ "max": "2026-03",
402
+ "min": "1990-01"
403
+ }
404
+ },
405
+ "currency": "SEK",
406
+ "granularities": [
407
+ "monthly",
408
+ "annual"
409
+ ]
410
+ },
411
+ {
412
+ "code": "SG",
413
+ "cpi_file": "cpi/sg.json",
414
+ "cpi_ranges": {
415
+ "annual": {
416
+ "max": "2025",
417
+ "min": "1960"
418
+ },
419
+ "monthly": {
420
+ "max": "2026-03",
421
+ "min": "2005-01"
422
+ }
423
+ },
424
+ "currency": "SGD",
425
+ "granularities": [
426
+ "monthly",
427
+ "annual"
428
+ ]
429
+ },
430
+ {
431
+ "code": "TH",
432
+ "cpi_file": "cpi/th.json",
433
+ "cpi_ranges": {
434
+ "annual": {
435
+ "max": "2025",
436
+ "min": "1960"
437
+ },
438
+ "monthly": {
439
+ "max": "2026-03",
440
+ "min": "2010-01"
441
+ }
442
+ },
443
+ "currency": "THB",
444
+ "granularities": [
445
+ "monthly",
446
+ "annual"
447
+ ]
448
+ },
449
+ {
450
+ "code": "TR",
451
+ "cpi_file": "cpi/tr.json",
452
+ "cpi_ranges": {
453
+ "annual": {
454
+ "max": "2025",
455
+ "min": "1960"
456
+ },
457
+ "monthly": {
458
+ "max": "2026-03",
459
+ "min": "1990-01"
460
+ }
461
+ },
462
+ "currency": "TRY",
463
+ "granularities": [
464
+ "monthly",
465
+ "annual"
466
+ ]
467
+ },
20
468
  {
21
469
  "code": "UK",
22
470
  "cpi_file": "cpi/uk.json",
471
+ "cpi_ranges": {
472
+ "annual": {
473
+ "max": "2025",
474
+ "min": "1988"
475
+ },
476
+ "monthly": {
477
+ "max": "2026-03",
478
+ "min": "1988-01"
479
+ }
480
+ },
23
481
  "currency": "GBP",
24
482
  "granularities": [
25
483
  "monthly",
@@ -29,6 +487,16 @@
29
487
  {
30
488
  "code": "US",
31
489
  "cpi_file": "cpi/us.json",
490
+ "cpi_ranges": {
491
+ "annual": {
492
+ "max": "2024",
493
+ "min": "1913"
494
+ },
495
+ "monthly": {
496
+ "max": "2026-04",
497
+ "min": "1913-01"
498
+ }
499
+ },
32
500
  "currency": "USD",
33
501
  "granularities": [
34
502
  "monthly",
@@ -38,22 +506,117 @@
38
506
  {
39
507
  "code": "VN",
40
508
  "cpi_file": "cpi/vn.json",
509
+ "cpi_ranges": {
510
+ "annual": {
511
+ "max": "2025",
512
+ "min": "1995"
513
+ },
514
+ "monthly": {
515
+ "max": "2026-03",
516
+ "min": "2001-12"
517
+ }
518
+ },
41
519
  "currency": "VND",
42
520
  "granularities": [
43
521
  "monthly",
44
522
  "annual"
45
523
  ]
524
+ },
525
+ {
526
+ "code": "ZA",
527
+ "cpi_file": "cpi/za.json",
528
+ "cpi_ranges": {
529
+ "annual": {
530
+ "max": "2025",
531
+ "min": "1960"
532
+ },
533
+ "monthly": {
534
+ "max": "2026-03",
535
+ "min": "1990-01"
536
+ }
537
+ },
538
+ "currency": "ZAR",
539
+ "granularities": [
540
+ "monthly",
541
+ "annual"
542
+ ]
46
543
  }
47
544
  ],
48
545
  "fx": {
49
546
  "annual_file": "fx/usd/_annual.json",
547
+ "annual_only_currencies": [
548
+ "RUB",
549
+ "VND"
550
+ ],
50
551
  "base": "USD",
51
552
  "currencies": [
553
+ "AUD",
554
+ "BGN",
555
+ "BRL",
556
+ "CAD",
557
+ "CHF",
558
+ "CNY",
559
+ "CZK",
560
+ "DKK",
52
561
  "EUR",
53
562
  "GBP",
563
+ "HKD",
564
+ "HUF",
565
+ "IDR",
566
+ "ILS",
567
+ "INR",
568
+ "ISK",
54
569
  "JPY",
55
- "VND"
570
+ "KRW",
571
+ "MXN",
572
+ "MYR",
573
+ "NOK",
574
+ "NZD",
575
+ "PHP",
576
+ "PLN",
577
+ "RON",
578
+ "RUB",
579
+ "SEK",
580
+ "SGD",
581
+ "THB",
582
+ "TRY",
583
+ "VND",
584
+ "ZAR"
585
+ ],
586
+ "daily_currencies": [
587
+ "AUD",
588
+ "BGN",
589
+ "BRL",
590
+ "CAD",
591
+ "CHF",
592
+ "CNY",
593
+ "CZK",
594
+ "DKK",
595
+ "EUR",
596
+ "GBP",
597
+ "HKD",
598
+ "HUF",
599
+ "IDR",
600
+ "ILS",
601
+ "INR",
602
+ "ISK",
603
+ "JPY",
604
+ "KRW",
605
+ "MXN",
606
+ "MYR",
607
+ "NOK",
608
+ "NZD",
609
+ "PHP",
610
+ "PLN",
611
+ "RON",
612
+ "SEK",
613
+ "SGD",
614
+ "THB",
615
+ "TRY",
616
+ "ZAR"
56
617
  ],
618
+ "daily_max": "2026-05-15",
619
+ "daily_min": "1999-01-04",
57
620
  "daily_years": [
58
621
  1999,
59
622
  2000,
@@ -85,6 +648,6 @@
85
648
  2026
86
649
  ]
87
650
  },
88
- "generated_at": "2026-05-11",
651
+ "generated_at": "2026-05-16",
89
652
  "schema_version": 4
90
653
  }
@@ -15,30 +15,66 @@ module Timeprice
15
15
  end
16
16
 
17
17
  def json_hash
18
- @result.to_h.merge(
18
+ base = @result.to_h.merge(
19
19
  amount: round_money(@result.amount, @result.to_currency),
20
20
  original_amount: round_money(@result.original_amount, @result.from_currency),
21
21
  converted_amount: round_money(@result.converted_amount, @result.to_currency),
22
22
  fx_rate: @result.fx_rate.to_f.round(6),
23
23
  cpi_ratio: @result.cpi_ratio.to_f.round(6)
24
24
  )
25
+ if @result.forecast
26
+ fc = @result.forecast
27
+ base[:forecast] = fc.merge(
28
+ low: round_money(fc[:low], @result.to_currency),
29
+ high: round_money(fc[:high], @result.to_currency)
30
+ )
31
+ else
32
+ base.delete(:forecast)
33
+ end
34
+ base
25
35
  end
26
36
 
27
37
  # Headline + left-to-right chain so the FX + CPI composition reads naturally.
28
38
  def text_lines
29
- final = "#{fmt_money(@result.amount, @result.to_currency)} #{@result.to_currency}"
30
- original = "#{fmt_money(@result.original_amount, @result.from_currency)} #{@result.from_currency}"
39
+ final = "#{fmt_money(@result.amount, @result.to_currency)} #{@result.to_currency}"
40
+ original = "#{fmt_money(@result.original_amount, @result.from_currency)} #{@result.from_currency}"
31
41
  converted = "#{fmt_money(@result.converted_amount, @result.to_currency)} #{@result.to_currency}"
32
- step1 = "fx @ #{fmt_rate(@result.fx_rate)}"
33
- step2 = "inflate x#{format("%.4f", @result.cpi_ratio)} #{@result.country}"
34
- width = [step1.length, step2.length].max
35
- [
36
- "#{final} in #{@result.to_date}",
42
+ step1 = "fx @ #{fmt_rate(@result.fx_rate)}"
43
+ step2 = "inflate x#{format("%.4f", @result.cpi_ratio)} #{@result.country}"
44
+ width = [step1.length, step2.length].max
45
+ headline = if @result.forecast
46
+ "#{final} in #{@result.to_date} (forecast)"
47
+ else
48
+ "#{final} in #{@result.to_date}"
49
+ end
50
+ lines = [
51
+ headline,
37
52
  " #{original} (#{@result.from_date})",
38
53
  format(" -> %-#{width}s -> %s (%s)", step1, converted, @result.from_date),
39
54
  format(" -> %-#{width}s -> %s (%s, %s)", step2, final, @result.to_date,
40
55
  Granularity.humanize(@result.granularity)),
41
56
  ]
57
+ @result.forecast ? lines + forecast_lines(final) : lines
58
+ end
59
+
60
+ private
61
+
62
+ def forecast_lines(mid_str)
63
+ fc = @result.forecast
64
+ low_str = "#{fmt_money(fc[:low], @result.to_currency)} #{@result.to_currency}"
65
+ high_str = "#{fmt_money(fc[:high], @result.to_currency)} #{@result.to_currency}"
66
+ extra = [
67
+ "",
68
+ " range #{low_str} — #{mid_str} — #{high_str}",
69
+ " (low -1σ) (most likely) (high +1σ)",
70
+ "",
71
+ " basis trailing #{fc[:window_years]}y CAGR · last data #{fc[:last_known_date]}",
72
+ " sigma ±#{format("%.1f", fc[:sigma_pct] * 100)}%/yr · horizon +#{fc[:horizon_months]}mo",
73
+ ]
74
+ if fc[:warnings].include?("horizon_exceeds_cap")
75
+ extra << " caveat long-horizon forecast: results are illustrative, not predictive"
76
+ end
77
+ extra
42
78
  end
43
79
  end
44
80
  end