diff --git a/src/main/java/com/lincfusion/udyamlinc/ingestion/NoFeedPriceSimulationService.java b/src/main/java/com/lincfusion/udyamlinc/ingestion/NoFeedPriceSimulationService.java index 7785546..f7a14c2 100644 --- a/src/main/java/com/lincfusion/udyamlinc/ingestion/NoFeedPriceSimulationService.java +++ b/src/main/java/com/lincfusion/udyamlinc/ingestion/NoFeedPriceSimulationService.java @@ -197,6 +197,64 @@ public class NoFeedPriceSimulationService { private static final GroupShape DEFAULT_SHAPE = new GroupShape(0.12, 0.020, 0.045); + /** + * The ISO week each commodity's price peaks — which is not when it is + * harvested, and getting that backwards inverts every series. + * + *
A mandi price troughs at the arrival glut and peaks in the lean season + * before the next one. Wheat is harvested in March–April and is cheapest then; + * it is dearest in November–December, when last season's stock has run down and + * the new crop is still in the ground. So the entry for wheat is week 48, not + * week 14. The same inversion applies right down the list: rabi crops peak in + * autumn, kharif crops peak in early summer, and winter vegetables peak in the + * monsoon when they cannot be grown. + * + *
Why this matters beyond looking right. {@code margin_squeeze} reads a + * sector's OUTPUT price against its INPUT price, and a squeeze is real when the + * two peak at different times of year. Deriving the peak from a hash of the + * commodity code — which is what this replaces — put those peaks in arbitrary + * places, so a poultry unit could show feed and egg peaking in the same week and + * a dairy could show them six months apart, both by accident. The seasonal + * relationship between an input and an output is the thing the derivation exists + * to measure, so it has to come from a calendar rather than from a hash. + * + *
Approximate and national, and both caveats are real. Harvest windows shift
+ * by weeks across fourteen districts spanning six zones — Bihar's paddy and
+ * Karnataka's are not the same week — and this table carries one figure for
+ * each. It is a better approximation than a hash, not a crop calendar for a
+ * specific district. Anything reading these rows sees {@code SIMULATED}.
+ */
+ private static final Map An explicit hand-anchored profile wins; otherwise the level comes from
* {@link #CROP_BASE} and the shape from the commodity's group.
*
- * The peak week is derived from the commodity code rather than from a crop
- * calendar. That is a real limitation and worth naming: the series will have a
- * consistent, deterministic season, but not necessarily the season —
- * onion's simulated peak is not guaranteed to fall in the actual rabi arrival
- * window. It matters for realism rather than for correctness, since every
- * derivation reads the series against itself. Replacing this with a sowing and
- * harvest calendar is the obvious improvement once one is on hand.
+ * The peak week comes from {@link #PRICE_PEAK_WEEK}, the lean-season table.
+ * A commodity missing from it falls back to a hash of its code, which keeps the
+ * series deterministic but puts its season somewhere arbitrary — acceptable for
+ * a commodity nothing reads as a sector's input or output, and a warning that a
+ * calendar entry is missing if it ever is.
*/
private Profile profileFor(Commodity commodity) {
Profile explicit = PROFILES.get(commodity.getCode());
@@ -276,7 +332,13 @@ public class NoFeedPriceSimulationService {
}
GroupShape shape = GROUP_SHAPES.getOrDefault(commodity.getCommodityGroup(), DEFAULT_SHAPE);
double base = CROP_BASE.getOrDefault(commodity.getCode(), 3000.0);
- int peakWeek = Math.floorMod(commodity.getCode().hashCode(), 52) + 1;
+ Integer calendarPeak = PRICE_PEAK_WEEK.get(commodity.getCode());
+ if (calendarPeak == null) {
+ log.warn("No price-peak week for commodity {} — falling back to a hashed season", commodity.getCode());
+ }
+ int peakWeek = calendarPeak != null
+ ? calendarPeak
+ : Math.floorMod(commodity.getCode().hashCode(), 52) + 1;
return new Profile(base, shape.seasonalAmplitude(), shape.trendPerYear(), peakWeek, shape.weeklyNoise());
}