@sybilion/uilib 1.3.64 → 1.3.66
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.
- package/dist/agent-glossary/content.generated.ts +253 -0
- package/dist/agent-glossary/content.md +252 -0
- package/dist/agent-glossary/workspace.generated.ts +272 -0
- package/dist/agent-glossary/workspace.md +271 -0
- package/dist/esm/components/ui/Chart/components/QuantileBands.js +1 -1
- package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.js +60 -1
- package/dist/esm/components/widgets/PerformanceChart/performanceChart.helpers.js +132 -33
- package/dist/esm/index.js +3 -1
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.d.ts +3 -2
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/index.d.ts +2 -0
- package/dist/esm/types/src/components/widgets/PerformanceChart/index.d.ts +1 -1
- package/dist/esm/types/src/components/widgets/PerformanceChart/performanceChart.helpers.d.ts +22 -2
- package/package.json +4 -2
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.ts +4 -3
- package/src/components/ui/ChartAreaInteractive/index.ts +2 -0
- package/src/components/ui/Page/AGENT.md +165 -0
- package/src/components/widgets/AGENT.md +1 -1
- package/src/components/widgets/PerformanceChart/index.ts +3 -0
- package/src/components/widgets/PerformanceChart/performanceChart.helpers.ts +197 -41
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
6
6
|
import type { ForecastData } from '#uilib/types/forecast-data';
|
|
7
7
|
import {
|
|
8
|
+
getNextMonth,
|
|
8
9
|
getPreviousMonth,
|
|
9
10
|
normalizeToMonthStart,
|
|
10
11
|
} from '#uilib/utils/chartConnectionPoint';
|
|
@@ -67,19 +68,16 @@ function addSpaghettiHistoricalBridgeForSeries(
|
|
|
67
68
|
map.set(connectionDate, row);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
*/
|
|
75
|
-
|
|
76
|
-
forecastRoot:
|
|
77
|
-
| { forecasts?: Record<string, Record<string, number>> }
|
|
78
|
-
| null
|
|
79
|
-
| undefined,
|
|
71
|
+
type PerHorizonForecastRoot = {
|
|
72
|
+
forecasts?: Record<string, Record<string, number>>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** When horizons have different lengths, align on the latest `n` months (suffix), not the oldest. */
|
|
76
|
+
function perHorizonSortedKeyLists(
|
|
77
|
+
forecastRoot: PerHorizonForecastRoot,
|
|
80
78
|
horizonKeys: string[],
|
|
81
|
-
):
|
|
82
|
-
if (!forecastRoot
|
|
79
|
+
): { sortedHorizons: string[]; perHorizonKeyLists: string[][] } | null {
|
|
80
|
+
if (!forecastRoot.forecasts || horizonKeys.length === 0) return null;
|
|
83
81
|
|
|
84
82
|
const forecasts = forecastRoot.forecasts;
|
|
85
83
|
const sortedHorizons = [...horizonKeys].sort((a, b) => {
|
|
@@ -99,16 +97,57 @@ export function buildPerHorizonSpaghettiEntries(
|
|
|
99
97
|
});
|
|
100
98
|
|
|
101
99
|
const lengths = perHorizonKeyLists.map(l => l.length).filter(l => l > 0);
|
|
102
|
-
if (lengths.length === 0) return
|
|
103
|
-
|
|
100
|
+
if (lengths.length === 0) return null;
|
|
101
|
+
|
|
102
|
+
return { sortedHorizons, perHorizonKeyLists };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function alignedHorizonRowCount(perHorizonKeyLists: string[][]): number {
|
|
106
|
+
const lengths = perHorizonKeyLists.map(l => l.length).filter(l => l > 0);
|
|
107
|
+
if (lengths.length === 0) return 0;
|
|
108
|
+
return Math.min(...lengths);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function horizonDateKeyAtRow(
|
|
112
|
+
perHorizonKeyLists: string[][],
|
|
113
|
+
horizonIndex: number,
|
|
114
|
+
rowIndex: number,
|
|
115
|
+
rowCount: number,
|
|
116
|
+
): string | undefined {
|
|
117
|
+
const keys = perHorizonKeyLists[horizonIndex];
|
|
118
|
+
if (!keys?.length) return undefined;
|
|
119
|
+
const offset = keys.length - rowCount;
|
|
120
|
+
return keys[offset + rowIndex];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Converts `performance.model` / `performance.drift` per-horizon forecasts into synthetic
|
|
125
|
+
* backtest-shaped entries: each line is [horizon_1[i], …, horizon_n[i]] as date→value points
|
|
126
|
+
* (aligned on the latest shared months per horizon).
|
|
127
|
+
*/
|
|
128
|
+
export function buildPerHorizonSpaghettiEntries(
|
|
129
|
+
forecastRoot:
|
|
130
|
+
| { forecasts?: Record<string, Record<string, number>> }
|
|
131
|
+
| null
|
|
132
|
+
| undefined,
|
|
133
|
+
horizonKeys: string[],
|
|
134
|
+
): RealBacktestsEntry[] {
|
|
135
|
+
const aligned = perHorizonSortedKeyLists(forecastRoot ?? {}, horizonKeys);
|
|
136
|
+
if (!aligned) return [];
|
|
137
|
+
|
|
138
|
+
const { sortedHorizons, perHorizonKeyLists } = aligned;
|
|
139
|
+
const forecasts = forecastRoot!.forecasts!;
|
|
140
|
+
const normalizeDateKey = (d: string) => d.split(' ')[0];
|
|
141
|
+
const n = alignedHorizonRowCount(perHorizonKeyLists);
|
|
142
|
+
if (n === 0) return [];
|
|
104
143
|
|
|
105
144
|
const entries: RealBacktestsEntry[] = [];
|
|
106
145
|
for (let i = 0; i < n; i++) {
|
|
107
146
|
const forecast_series: Record<string, number> = {};
|
|
108
147
|
for (let hi = 0; hi < sortedHorizons.length; hi++) {
|
|
109
148
|
const h = sortedHorizons[hi];
|
|
110
|
-
const
|
|
111
|
-
|
|
149
|
+
const dateKey = horizonDateKeyAtRow(perHorizonKeyLists, hi, i, n);
|
|
150
|
+
if (!dateKey) continue;
|
|
112
151
|
const rawVal = forecasts[h]?.[dateKey];
|
|
113
152
|
if (typeof rawVal !== 'number' || !Number.isFinite(rawVal)) continue;
|
|
114
153
|
const norm = normalizeToMonthStart(normalizeDateKey(dateKey));
|
|
@@ -127,7 +166,7 @@ export function buildPerHorizonSpaghettiEntries(
|
|
|
127
166
|
|
|
128
167
|
/**
|
|
129
168
|
* Same row alignment as {@link buildPerHorizonSpaghettiEntries}: row `i` uses each horizon's
|
|
130
|
-
*
|
|
169
|
+
* latest-aligned forecast month; `dates[i]` is horizon_1's month at that row (Date column).
|
|
131
170
|
* Use this for custom dialog seed + “copy statistical baseline (drift)” prefill.
|
|
132
171
|
*/
|
|
133
172
|
export function buildDriftSpaghettiMatrixForCustomDialog(
|
|
@@ -141,28 +180,14 @@ export function buildDriftSpaghettiMatrixForCustomDialog(
|
|
|
141
180
|
grid: number[][];
|
|
142
181
|
perHorizonDates: string[][];
|
|
143
182
|
} | null {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const forecasts = driftRoot.forecasts;
|
|
147
|
-
const sortedHorizons = [...horizonKeys].sort((a, b) => {
|
|
148
|
-
const na = parseInt(a.replace(/\D/g, ''), 10) || 0;
|
|
149
|
-
const nb = parseInt(b.replace(/\D/g, ''), 10) || 0;
|
|
150
|
-
return na - nb;
|
|
151
|
-
});
|
|
183
|
+
const aligned = perHorizonSortedKeyLists(driftRoot ?? {}, horizonKeys);
|
|
184
|
+
if (!aligned) return null;
|
|
152
185
|
|
|
186
|
+
const { sortedHorizons, perHorizonKeyLists } = aligned;
|
|
187
|
+
const forecasts = driftRoot!.forecasts!;
|
|
153
188
|
const normalizeDateKey = (d: string) => d.split(' ')[0];
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const m = forecasts[h];
|
|
157
|
-
if (!m || typeof m !== 'object') return [];
|
|
158
|
-
return Object.keys(m).sort((a, b) =>
|
|
159
|
-
normalizeDateKey(a).localeCompare(normalizeDateKey(b)),
|
|
160
|
-
);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const lengths = perHorizonKeyLists.map(l => l.length).filter(l => l > 0);
|
|
164
|
-
if (lengths.length === 0) return null;
|
|
165
|
-
const n = Math.min(...lengths);
|
|
189
|
+
const n = alignedHorizonRowCount(perHorizonKeyLists);
|
|
190
|
+
if (n === 0) return null;
|
|
166
191
|
|
|
167
192
|
const dates: string[] = [];
|
|
168
193
|
const grid: number[][] = [];
|
|
@@ -173,16 +198,20 @@ export function buildDriftSpaghettiMatrixForCustomDialog(
|
|
|
173
198
|
const dateRow: string[] = [];
|
|
174
199
|
for (let hi = 0; hi < sortedHorizons.length; hi++) {
|
|
175
200
|
const h = sortedHorizons[hi];
|
|
176
|
-
const
|
|
177
|
-
|
|
201
|
+
const dateKey = horizonDateKeyAtRow(perHorizonKeyLists, hi, i, n);
|
|
202
|
+
if (!dateKey) {
|
|
203
|
+
row.push(0);
|
|
204
|
+
dateRow.push('');
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
178
207
|
dateRow.push(normalizeToMonthStart(normalizeDateKey(dateKey)));
|
|
179
208
|
const rawVal = forecasts[h]?.[dateKey];
|
|
180
209
|
const v =
|
|
181
210
|
typeof rawVal === 'number' && Number.isFinite(rawVal) ? rawVal : NaN;
|
|
182
211
|
row.push(v);
|
|
183
212
|
}
|
|
184
|
-
const d0 = perHorizonKeyLists
|
|
185
|
-
dates.push(normalizeToMonthStart(normalizeDateKey(d0)));
|
|
213
|
+
const d0 = horizonDateKeyAtRow(perHorizonKeyLists, 0, i, n);
|
|
214
|
+
dates.push(d0 ? normalizeToMonthStart(normalizeDateKey(d0)) : '');
|
|
186
215
|
perHorizonDates.push(dateRow);
|
|
187
216
|
grid.push(row.map(c => (Number.isFinite(c) ? c : 0)));
|
|
188
217
|
}
|
|
@@ -194,6 +223,133 @@ export function buildDriftSpaghettiMatrixForCustomDialog(
|
|
|
194
223
|
};
|
|
195
224
|
}
|
|
196
225
|
|
|
226
|
+
export function latestHistoricalMonthKey(
|
|
227
|
+
historicalByDate: Map<string, number>,
|
|
228
|
+
): string | null {
|
|
229
|
+
let latest: string | null = null;
|
|
230
|
+
historicalByDate.forEach((_, d) => {
|
|
231
|
+
if (!latest || d.localeCompare(latest) > 0) latest = d;
|
|
232
|
+
});
|
|
233
|
+
return latest;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Append monthly spaghetti rows after drift backtests so the dialog (and chart) reach the latest
|
|
238
|
+
* historical month (e.g. Apr 2026), not only the last drift origin (e.g. Nov 2025).
|
|
239
|
+
*/
|
|
240
|
+
export function extendCustomPerformanceDriftSeedToHistoricalEnd(
|
|
241
|
+
seed: {
|
|
242
|
+
dates: string[];
|
|
243
|
+
grid: number[][];
|
|
244
|
+
perHorizonDates: string[][];
|
|
245
|
+
},
|
|
246
|
+
historicalByDate: Map<string, number>,
|
|
247
|
+
): {
|
|
248
|
+
dates: string[];
|
|
249
|
+
grid: number[][];
|
|
250
|
+
perHorizonDates: string[][];
|
|
251
|
+
} {
|
|
252
|
+
const latest = latestHistoricalMonthKey(historicalByDate);
|
|
253
|
+
if (!latest || seed.dates.length === 0) return seed;
|
|
254
|
+
|
|
255
|
+
const norm = (d: string) =>
|
|
256
|
+
normalizeToMonthStart(String(d).split(' ')[0] ?? d);
|
|
257
|
+
|
|
258
|
+
const dates = seed.dates.map(d => norm(d));
|
|
259
|
+
const grid = seed.grid.map(r => [...r]);
|
|
260
|
+
const perHorizonDates = seed.perHorizonDates.map(row =>
|
|
261
|
+
row.map(d => norm(String(d))),
|
|
262
|
+
);
|
|
263
|
+
const horizonCount = grid[0]?.length ?? perHorizonDates[0]?.length ?? 0;
|
|
264
|
+
if (horizonCount === 0) return seed;
|
|
265
|
+
|
|
266
|
+
let lastOrigin = dates[dates.length - 1];
|
|
267
|
+
if (!lastOrigin) return seed;
|
|
268
|
+
|
|
269
|
+
while (lastOrigin.localeCompare(latest) < 0) {
|
|
270
|
+
const nextOrigin = norm(getNextMonth(lastOrigin));
|
|
271
|
+
const prevPh = perHorizonDates[perHorizonDates.length - 1];
|
|
272
|
+
const newPh =
|
|
273
|
+
prevPh.length === horizonCount
|
|
274
|
+
? prevPh.map(d => norm(getNextMonth(norm(d))))
|
|
275
|
+
: Array.from({ length: horizonCount }, () => nextOrigin);
|
|
276
|
+
|
|
277
|
+
perHorizonDates.push(newPh);
|
|
278
|
+
dates.push(nextOrigin);
|
|
279
|
+
|
|
280
|
+
const baselineRow = spaghettiGridFromHistoricalPreviousMonth(
|
|
281
|
+
[newPh],
|
|
282
|
+
horizonCount,
|
|
283
|
+
historicalByDate,
|
|
284
|
+
);
|
|
285
|
+
grid.push(baselineRow[0] ?? Array.from({ length: horizonCount }, () => 0));
|
|
286
|
+
|
|
287
|
+
lastOrigin = nextOrigin;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { dates, grid, perHorizonDates };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Pad a saved custom matrix with drift/baseline rows so edit + chart cover latest backtest months. */
|
|
294
|
+
export function extendCustomPerformanceMatrixWithDriftSeed(
|
|
295
|
+
saved: SpaghettiPerformanceMatrixPayload,
|
|
296
|
+
driftSeed: {
|
|
297
|
+
dates: string[];
|
|
298
|
+
grid: number[][];
|
|
299
|
+
perHorizonDates: string[][];
|
|
300
|
+
},
|
|
301
|
+
baselineGrid?: number[][],
|
|
302
|
+
): SpaghettiPerformanceMatrixPayload {
|
|
303
|
+
const norm = (d: string) =>
|
|
304
|
+
normalizeToMonthStart(String(d).split(' ')[0] ?? d);
|
|
305
|
+
const horizonCount = saved.horizonKeys.length;
|
|
306
|
+
|
|
307
|
+
const savedByDate = new Map<
|
|
308
|
+
string,
|
|
309
|
+
{ grid: number[]; perHorizon?: string[] }
|
|
310
|
+
>();
|
|
311
|
+
for (let r = 0; r < saved.dates.length; r++) {
|
|
312
|
+
savedByDate.set(norm(saved.dates[r]), {
|
|
313
|
+
grid: saved.grid[r],
|
|
314
|
+
perHorizon: saved.perHorizonDates?.[r],
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const dates: string[] = [];
|
|
319
|
+
const grid: number[][] = [];
|
|
320
|
+
const perHorizonDates: string[][] = [];
|
|
321
|
+
|
|
322
|
+
for (let i = 0; i < driftSeed.dates.length; i++) {
|
|
323
|
+
const d = norm(driftSeed.dates[i]);
|
|
324
|
+
if (!d) continue;
|
|
325
|
+
dates.push(d);
|
|
326
|
+
const existing = savedByDate.get(d);
|
|
327
|
+
if (existing) {
|
|
328
|
+
grid.push([...existing.grid]);
|
|
329
|
+
const ph =
|
|
330
|
+
existing.perHorizon && existing.perHorizon.length === horizonCount
|
|
331
|
+
? existing.perHorizon.map(c => norm(String(c)))
|
|
332
|
+
: (driftSeed.perHorizonDates[i]?.map(c => norm(String(c))) ?? []);
|
|
333
|
+
perHorizonDates.push(ph);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
grid.push(
|
|
337
|
+
baselineGrid?.[i] ? [...baselineGrid[i]] : [...driftSeed.grid[i]],
|
|
338
|
+
);
|
|
339
|
+
perHorizonDates.push(
|
|
340
|
+
(driftSeed.perHorizonDates[i] ?? []).map(c => norm(String(c))),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
v: saved.v,
|
|
346
|
+
dates,
|
|
347
|
+
horizonKeys: [...saved.horizonKeys],
|
|
348
|
+
grid,
|
|
349
|
+
perHorizonDates,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
197
353
|
/**
|
|
198
354
|
* Prefill for custom performance when copying drift layout: each spaghetti row is flat at the
|
|
199
355
|
* historical value for the month before the earliest forecast month in that row (same anchor as
|