docs: add design spec for standalone underlying-only chart endpoint

Spec for a new /api/chart/underlying/{idUnderlyings} endpoint that
renders a single price-history line for one underlying, independent
of any certificate context (no strike/barriers/legend).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 10:17:39 +02:00
parent 04b291a7ef
commit 27784bcb4f

View File

@@ -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<UnderlyingChartPoint> 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<IUnderlyingChartDataService, UnderlyingChartDataService>()`.
### 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<IActionResult> 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).