fix: null-safe string reader, chart V2 label collision avoidance

- ChartDataServiceV2: aggiunto helper ToStr() per GetString null-safe su
  colonne nullable (Sottostante, NomeCFT); pattern analogo a ToDecimal
- SkiaChartRendererV2: collision avoidance label margine destro — tutti i
  label (barriere, strike, autocall, end-label serie) raccolti in lista,
  ordinati per Y e distribuiti con spacing minimo 13px (push-down +
  clamp-up) prima del disegno
- CLAUDE.md: documentati i due fix e la root cause cedlab_Chart_UL1
  divide-by-zero su Strike=0 (fix DB: NULLIF(cu.Strike,0))

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 10:27:44 +02:00
parent bb86c6ac20
commit c655cdd31e
3 changed files with 61 additions and 17 deletions

View File

@@ -86,6 +86,8 @@ HTTP (ISIN) → ReportController → ReportOrchestrator
| Namespace conflict `CertReports.Syncfusion.Pdf` | Aggiungere `using Syncfusion.Pdf;` esplicito nel file |
| SkiaSharp `SKFont` NON è IDisposable | Non usare `using` su `SKFont` — solo `SKPaint`, `SKPath`, `SKSurface`, `SKImage`, `SKData` sono IDisposable |
| `SqlDataReader.GetDecimal` su colonna `float` SQL | Usare `Convert.ToDecimal(r.GetValue(ord))` — le SP cedlab_ possono restituire `float` (es. `PriceWorst` da subquery su `Prices.Px_close`) |
| `SqlDataReader.GetString` su colonna nullable | Usare `ToStr(r, column)` (helper in `ChartDataServiceV2`) che fa `r.IsDBNull` prima — `GetString` su `DBNull` lancia `SqlNullValueException` |
| `cedlab_Chart_UL1`: Divide by zero su `Strike = 0` | La SP divide per `cu.Strike` senza guard nel calcolo `PriceWorstPerc`. Fix applicato nel DB: `/ NULLIF(cu.Strike, 0)`. Colpisce certificati con Strike non impostato in `CertificatesUnderlyings` |
## Grafico V2 (`/api/chart/v2/{isin}`)
@@ -126,6 +128,8 @@ Percorsi configurati in `appsettings.json` → `ChartSettings:SavePath` e `Chart
**Note renderer**: CTF non mostrato se `NumPrezziCFT < 30` (mostra solo avviso testo); asse X usa intervalli mensili adattivi (12m/6m/3m/1m in base al range).
**Label destra — collision avoidance**: i label sul margine destro (barriere, strike, autocall, end-label serie) vengono raccolti in una lista `rightLabels`, ordinati per Y, e disegnati da `DrawRightLabels` con spacing minimo 13px. Due passate: discendente (push-down) + risalente (clamp al bordo area). Questo evita la sovrapposizione quando una serie si accavalla con una linea costante.
## Database
Tutte le stored procedure sono su `FirstSolutionDB`. I dati tornano **già formattati come stringhe** dalle SP (es. `FORMAT(value,'P2','it-IT')`): i modelli C# usano `string` per questi campi. Solo `NominalValue`, `PrezzoEmissione`, `CpnPagati`, `CpnDaPagare`, `CpnInMemoria` sono `decimal?` perché servono come valori numerici nel rendering.