diff --git a/docs/superpowers/specs/2026-07-21-underlying-only-chart-design.md b/docs/superpowers/specs/2026-07-21-underlying-only-chart-design.md new file mode 100644 index 0000000..bcd307d --- /dev/null +++ b/docs/superpowers/specs/2026-07-21-underlying-only-chart-design.md @@ -0,0 +1,138 @@ +# Design: grafico standalone per singolo sottostante (senza certificato) + +**Data**: 2026-07-21 +**Stato**: approvato, in attesa di implementazione + +## Obiettivo + +Aggiungere un endpoint che generi un grafico semplificato mostrando **solo l'andamento storico prezzo** di un sottostante, identificato da `IDUnderlyings` — senza passare da un ISIN certificato e senza alcun riferimento a strike/barriere/trigger (che esistono solo nel contesto di un certificato). + +Caso d'uso: preview rapida dell'andamento di un sottostante indipendente da qualunque certificato collegato. + +## Architettura + +Stessa pipeline di `/api/chart/{isin}` e `/api/chart/v2/{isin}`, ma senza contesto certificato: + +``` +GET /api/chart/underlying/{idUnderlyings}?width=&height=&format= + → ChartController.GenerateChartUnderlying(idUnderlyings, width, height, format) + → IUnderlyingChartDataService.GetChartDataAsync(idUnderlyings) + → SP cedlab_Chart_UnderlyingOnly @IDUnderlyings + → SkiaChartRendererV2 (nuovo metodo per serie singola) + → PNG/JPEG bytes → risposta inline, oppure wrap in PDF (riuso WrapPngInPdf esistente) +``` + +## Componenti nuovi + +### 1. Modello — `Models/UnderlyingChartModels.cs` + +```csharp +public class UnderlyingChartData +{ + public int IDUnderlyings { get; set; } + public string Nome { get; set; } = string.Empty; // ticker/nome per il titolo + public List Points { get; set; } = new(); +} + +public class UnderlyingChartPoint +{ + public DateTime Date { get; set; } + public decimal Px { get; set; } +} +``` + +### 2. Servizio dati — `Services/Implementations/UnderlyingChartDataService.cs` + `IUnderlyingChartDataService` + +- Chiama la SP `cedlab_Chart_UnderlyingOnly @IDUnderlyings`. +- Popola `UnderlyingChartData` con i punti prezzo grezzi (non normalizzati a %, a differenza di v2 che divide per Strike/Nominal — qui non c'è contesto certificato per farlo). +- Registrato in `Program.cs` come `AddScoped()`. + +### 3. Stored Procedure — `docs/sql/cedlab_Chart_UnderlyingOnly.sql` + +```sql +CREATE OR ALTER PROCEDURE [dbo].[cedlab_Chart_UnderlyingOnly] + @IDUnderlyings INT +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @AdjustedPrices BIT; + SELECT @AdjustedPrices = ISNULL(AdjustedPrices, 0) + FROM dbo.Underlyings + WHERE IDUnderlyings = @IDUnderlyings + AND deleted = 0 + AND sospeso = 0; + + IF @AdjustedPrices IS NULL RETURN; -- non trovato / cancellato / sospeso + + ;WITH Ranked AS + ( + SELECT + p.Px_date, + CASE WHEN @AdjustedPrices = 0 THEN p.Px_close ELSE p.Px_closeadj END AS Px, + ROW_NUMBER() OVER (ORDER BY p.Px_date DESC) AS rn + FROM dbo.Prices p + WHERE p.UnderlyingsID = @IDUnderlyings + AND (p.Px_low IS NOT NULL OR p.Px_high IS NOT NULL OR + p.Px_open IS NOT NULL OR p.Px_close IS NOT NULL) + ) + SELECT + /* TODO: verificare colonna nome/ticker corretta in dbo.Underlyings prima di eseguire */ + u.Ticker AS Nome, + r.Px_date, + r.Px + FROM Ranked r + CROSS JOIN (SELECT TOP 1 * FROM dbo.Underlyings WHERE IDUnderlyings = @IDUnderlyings) u + WHERE r.rn <= 350 + ORDER BY r.Px_date ASC; +END +GO +``` + +> **Nota**: colonna `u.Ticker` è placeholder — il nome effettivo della colonna in `dbo.Underlyings` (Ticker / TickerBB / Nome / Description...) va confermato prima di applicare lo script al DB, come già fatto per `cedlab_Chart_AllSeriesV2.sql`. + +### 4. Renderer — nuovo metodo in `SkiaChartRendererV2` + +- Firma indicativa: `RenderSingleSeriesToPng(UnderlyingChartData data, int width, int height, bool jpeg)`. +- Riusa griglia, assi X con intervalli mensili adattivi (12m/6m/3m/1m) e stile generale di V2. +- Una sola linea (colore nero o `AccentBlue`, spessore 2px). +- Titolo in alto = `data.Nome`. +- **Nessuna legenda** (una sola serie non la richiede). +- Nessuna barriera/strike/trigger disegnati (non esiste contesto certificato). + +### 5. Controller — nuova route in `ChartController.cs` esistente + +``` +[HttpGet("underlying/{idUnderlyings}")] +public async Task GenerateChartUnderlying( + int idUnderlyings, + [FromQuery] int width = 1100, + [FromQuery] int height = 700, + [FromQuery] string format = "png") +``` + +- `idUnderlyings <= 0` → `400 BadRequest`. +- Nessun punto prezzo trovato → `404 NotFound` con messaggio esplicativo. +- Formati supportati: `png` (default), `jpg`/`jpeg`, `pdf` (via `WrapPngInPdf` esistente, riuso invariato). +- **Non supportato**: `jpgEnc` e `?save=true` — dipendono dall'alias-certificato (`rpt_CertificatesChartsAlias`) che non esiste per un sottostante puro senza contesto certificato. +- Eccezioni → `500` loggato, stesso pattern try/catch di v1/v2. + +## Error handling + +| Caso | Risposta | +|------|----------| +| `idUnderlyings <= 0` | 400 | +| Sottostante non trovato / `deleted=1` / `sospeso=1` | 404 | +| Nessun prezzo disponibile | 404 | +| Eccezione runtime | 500 (loggato) | + +## Cache + +Nessuna cache introdotta per questo endpoint — coerente con `/api/chart/{isin}` e `/api/chart/v2/{isin}`, che non cachano l'immagine (solo il PDF del report principale usa cache). Fuori scope. + +## Fuori scope + +- Parametro `?save=true` (nessun path di salvataggio dedicato per sottostanti puri). +- Formato `jpgEnc` (nessun alias-certificato applicabile). +- Barriere/strike/trigger/scenari — richiedono contesto certificato, non disponibile con solo `IDUnderlyings`. +- Normalizzazione a percentuale (richiede uno strike/nominal di riferimento, assente qui).