diff --git a/CertReports.Syncfusion/Services/Implementations/ChartDataServiceV2.cs b/CertReports.Syncfusion/Services/Implementations/ChartDataServiceV2.cs
new file mode 100644
index 0000000..1608d1c
--- /dev/null
+++ b/CertReports.Syncfusion/Services/Implementations/ChartDataServiceV2.cs
@@ -0,0 +1,105 @@
+using CertReports.Syncfusion.Models;
+using Microsoft.Data.SqlClient;
+using System.Data;
+
+namespace CertReports.Syncfusion.Services.Implementations;
+
+///
+/// Recupera i dati per il grafico V2 con solo 2 round-trip al DB.
+///
+/// SP utilizzate:
+/// - cedlab_Chart_UL1: Metadata sottostanti (1 query, N sottostanti)
+/// - cedlab_Chart_AllSeriesV2: Tutte le serie CTF + UL in una query (TOP 350 per-serie)
+///
+public interface IChartDataServiceV2
+{
+ Task GetChartDataV2Async(string isin);
+}
+
+public class ChartDataServiceV2 : IChartDataServiceV2
+{
+ private readonly string _connectionString;
+ private readonly ILogger _logger;
+
+ public ChartDataServiceV2(IConfiguration config, ILogger logger)
+ {
+ _connectionString = config.GetConnectionString("CertDb")
+ ?? throw new InvalidOperationException("ConnectionString 'CertDb' non configurata.");
+ _logger = logger;
+ }
+
+ public async Task GetChartDataV2Async(string isin)
+ {
+ await using var conn = new SqlConnection(_connectionString);
+ await conn.OpenAsync();
+
+ // ── 1. Metadata sottostanti (cedlab_Chart_UL1) ─────────────────
+ var underlyings = new List();
+
+ await using (var cmd = new SqlCommand("cedlab_Chart_UL1", conn)
+ { CommandType = CommandType.StoredProcedure })
+ {
+ cmd.Parameters.AddWithValue("@isin", isin);
+ await using var r = await cmd.ExecuteReaderAsync();
+ while (await r.ReadAsync())
+ {
+ underlyings.Add(new ChartUlMetadata
+ {
+ IDCertificates = r.GetInt32(r.GetOrdinal("IDCertificates")),
+ IDUnderlyings = r.GetInt32(r.GetOrdinal("IDUnderlyings")),
+ StartDate = r.GetDateTime(r.GetOrdinal("StartDate")),
+ Strike = r.GetDecimal(r.GetOrdinal("Strike")),
+ BarrieraCouponPerc = r.GetDecimal(r.GetOrdinal("BarrieraCouponPerc")),
+ BarrieraCoupon = r.GetDecimal(r.GetOrdinal("BarrieraCoupon")),
+ BarrieraCapitalePerc = r.GetDecimal(r.GetOrdinal("BarrieraCapitalePerc")),
+ BarrieraCapitale = r.GetDecimal(r.GetOrdinal("BarrieraCapitale")),
+ Sottostante = r.GetString(r.GetOrdinal("Sottostante")),
+ IsWorstOf = r.GetInt32(r.GetOrdinal("IsWorstOf")),
+ PriceWorst = r.GetDecimal(r.GetOrdinal("PriceWorst")),
+ PriceWorstPerc = r.GetDecimal(r.GetOrdinal("PriceWorstPerc")),
+ NumPrezziCFT = r.GetInt32(r.GetOrdinal("NumPrezziCFT")),
+ NomeCFT = r.GetString(r.GetOrdinal("NomeCFT")),
+ TriggerAutocallPerc = r.GetDecimal(r.GetOrdinal("TriggerAutocallPerc")),
+ AutocallValue = r.GetDecimal(r.GetOrdinal("AutocallValue")),
+ });
+ }
+ }
+
+ if (underlyings.Count == 0)
+ {
+ _logger.LogWarning(
+ "Nessun sottostante trovato per il grafico V2 di {Isin} (meno di 30 prezzi EOD?)", isin);
+ return null;
+ }
+
+ var result = new ChartDataV2
+ {
+ Isin = isin,
+ GlobalMeta = underlyings[0], // worst-of è il primo (SP ordina IsWorstOf DESC)
+ Underlyings = underlyings,
+ };
+
+ // ── 2. Tutte le serie (cedlab_Chart_AllSeriesV2) ────────────────
+ await using (var cmd = new SqlCommand("cedlab_Chart_AllSeriesV2", conn)
+ { CommandType = CommandType.StoredProcedure })
+ {
+ cmd.Parameters.AddWithValue("@isin", isin);
+ await using var r = await cmd.ExecuteReaderAsync();
+ while (await r.ReadAsync())
+ {
+ result.SeriesPoints.Add(new ChartSeriesPoint
+ {
+ IDUnderlyings = r.GetInt32(r.GetOrdinal("IDUnderlyings")),
+ Date = r.GetDateTime(r.GetOrdinal("Px_date")),
+ Performance = r.GetDecimal(r.GetOrdinal("Performance")),
+ });
+ }
+ }
+
+ _logger.LogInformation(
+ "Dati grafico V2 caricati per {Isin}: {UlCount} sottostanti, {Points} punti totali",
+ isin, underlyings.Count, result.SeriesPoints.Count);
+
+ return result;
+ }
+}