From e27b4c4a76c91b0ceaa52412bfa58f61ffc08420 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Thu, 17 Jul 2025 15:57:51 +0700 Subject: [PATCH] Add scoring message --- .../Models/Requests/LightBacktestResponse.cs | 1 + .../Backtesting/Backtester.cs | 5 +- src/Managing.Domain/Backtests/Backtest.cs | 1 + .../Backtests/BacktestScoringResult.cs | 114 ++++++++++++++ .../Backtests/LightBacktest.cs | 1 + .../Shared/Helpers/BacktestScore.cs | 139 ++++++++++++------ .../BacktestRepository.cs | 6 +- .../MongoDb/Collections/BacktestDto.cs | 1 + .../MongoDb/MongoMappers.cs | 2 + 9 files changed, 224 insertions(+), 46 deletions(-) create mode 100644 src/Managing.Domain/Backtests/BacktestScoringResult.cs diff --git a/src/Managing.Api/Models/Requests/LightBacktestResponse.cs b/src/Managing.Api/Models/Requests/LightBacktestResponse.cs index 6e0383a..ea32a24 100644 --- a/src/Managing.Api/Models/Requests/LightBacktestResponse.cs +++ b/src/Managing.Api/Models/Requests/LightBacktestResponse.cs @@ -16,4 +16,5 @@ public class LightBacktestResponse public decimal Fees { get; set; } public double? SharpeRatio { get; set; } public double Score { get; set; } + public string ScoreMessage { get; set; } = string.Empty; } \ No newline at end of file diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs index e454fb8..8836e1d 100644 --- a/src/Managing.Application/Backtesting/Backtester.cs +++ b/src/Managing.Application/Backtesting/Backtester.cs @@ -317,7 +317,7 @@ namespace Managing.Application.Backtesting timeframe: config.Timeframe ); - var score = BacktestScorer.CalculateTotalScore(scoringParams); + var scoringResult = BacktestScorer.CalculateDetailedScore(scoringParams); // Create backtest result with conditional candles and indicators values var result = new Backtest(config, bot.Positions, bot.Signals.ToList(), @@ -334,7 +334,8 @@ namespace Managing.Application.Backtesting IndicatorsValues = withCandles ? AggregateValues(indicatorsValues, bot.IndicatorsValues) : new Dictionary(), - Score = score, + Score = scoringResult.Score, + ScoreMessage = scoringResult.SummaryMessage, Id = Guid.NewGuid().ToString(), RequestId = requestId, Metadata = metadata, diff --git a/src/Managing.Domain/Backtests/Backtest.cs b/src/Managing.Domain/Backtests/Backtest.cs index 1430034..dffa0bf 100644 --- a/src/Managing.Domain/Backtests/Backtest.cs +++ b/src/Managing.Domain/Backtests/Backtest.cs @@ -60,6 +60,7 @@ public class Backtest [Required] public double Score { get; set; } public string RequestId { get; set; } public object? Metadata { get; set; } + public string ScoreMessage { get; set; } = string.Empty; /// /// Creates a new TradingBotConfig based on this backtest's configuration for starting a live bot. diff --git a/src/Managing.Domain/Backtests/BacktestScoringResult.cs b/src/Managing.Domain/Backtests/BacktestScoringResult.cs new file mode 100644 index 0000000..00c6250 --- /dev/null +++ b/src/Managing.Domain/Backtests/BacktestScoringResult.cs @@ -0,0 +1,114 @@ +using System.Text; + +namespace Managing.Domain.Backtests; + +public class BacktestScoringResult +{ + public double Score { get; set; } + public List Checks { get; set; } = new(); + public string SummaryMessage { get; set; } = string.Empty; + + public BacktestScoringResult(double score) + { + Score = score; + } + + public void AddCheck(string component, double score, double weight, string message, bool passed = true) + { + Checks.Add(new ScoringCheck + { + Component = component, + Score = score, + Weight = weight, + Message = message, + Passed = passed + }); + } + + public void AddEarlyExitCheck(string reason, string message) + { + Checks.Add(new ScoringCheck + { + Component = "Early Exit", + Score = 0, + Weight = 0, + Message = message, + Passed = false, + IsEarlyExit = true + }); + } + + public void AddPenaltyCheck(string component, double penaltyMultiplier, string message) + { + Checks.Add(new ScoringCheck + { + Component = component, + Score = 0, + Weight = 0, + Message = message, + Passed = penaltyMultiplier >= 1.0, + IsPenalty = true, + PenaltyMultiplier = penaltyMultiplier + }); + } + + public string GenerateSummaryMessage() + { + if (Score == 0) + { + var earlyExit = Checks.FirstOrDefault(c => c.IsEarlyExit); + if (earlyExit != null) + { + return $"Score: 0 - {earlyExit.Message}"; + } + } + + var passedChecks = Checks.Where(c => c.Passed && !c.IsEarlyExit && !c.IsPenalty).ToList(); + var failedChecks = Checks.Where(c => !c.Passed && !c.IsEarlyExit && !c.IsPenalty).ToList(); + var penalties = Checks.Where(c => c.IsPenalty).ToList(); + + var summary = new StringBuilder(); + summary.AppendLine($"Final Score: {Score:F1}/100"); + + if (passedChecks.Any()) + { + summary.AppendLine($"✅ Passed Checks ({passedChecks.Count}):"); + foreach (var check in passedChecks.OrderByDescending(c => c.Score * c.Weight)) + { + summary.AppendLine($" • {check.Component}: {check.Score:F1} points ({check.Message})"); + } + } + + if (failedChecks.Any()) + { + summary.AppendLine($"❌ Failed Checks ({failedChecks.Count}):"); + foreach (var check in failedChecks) + { + summary.AppendLine($" • {check.Component}: {check.Message}"); + } + } + + if (penalties.Any()) + { + summary.AppendLine($"⚠️ Applied Penalties ({penalties.Count}):"); + foreach (var penalty in penalties) + { + summary.AppendLine($" • {penalty.Component}: {penalty.Message}"); + } + } + + return summary.ToString().TrimEnd(); + } +} + +public class ScoringCheck +{ + public string Component { get; set; } = string.Empty; + public double Score { get; set; } + public double Weight { get; set; } + public string Message { get; set; } = string.Empty; + public bool Passed { get; set; } + public bool IsEarlyExit { get; set; } + public bool IsPenalty { get; set; } + public double PenaltyMultiplier { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Domain/Backtests/LightBacktest.cs b/src/Managing.Domain/Backtests/LightBacktest.cs index 25ad64e..cb26407 100644 --- a/src/Managing.Domain/Backtests/LightBacktest.cs +++ b/src/Managing.Domain/Backtests/LightBacktest.cs @@ -16,4 +16,5 @@ public class LightBacktest public decimal Fees { get; set; } public double? SharpeRatio { get; set; } public double Score { get; set; } + public string ScoreMessage { get; set; } = string.Empty; } \ No newline at end of file diff --git a/src/Managing.Domain/Shared/Helpers/BacktestScore.cs b/src/Managing.Domain/Shared/Helpers/BacktestScore.cs index da57ae4..6d307f3 100644 --- a/src/Managing.Domain/Shared/Helpers/BacktestScore.cs +++ b/src/Managing.Domain/Shared/Helpers/BacktestScore.cs @@ -21,58 +21,101 @@ public class BacktestScorer public static double CalculateTotalScore(BacktestScoringParams p) { + var result = CalculateDetailedScore(p); + return result.Score; + } + + public static BacktestScoringResult CalculateDetailedScore(BacktestScoringParams p) + { + var result = new BacktestScoringResult(0); + try { // Early exit for no positions if (p.TradeCount == 0) { - return 0; + result.AddEarlyExitCheck("No Trades", "No trading positions were taken during the backtest period. A strategy must execute trades to be evaluated."); + return result; } // Early exit for negative PnL - should be 0 score if (p.TotalPnL <= 0) { - return 0; + result.AddEarlyExitCheck("Negative PnL", $"Total profit/loss is negative (${p.TotalPnL:F2}). Only profitable strategies receive scores."); + return result; } // Early exit if strategy significantly underperforms HODL (more than 2% worse) if (p.GrowthPercentage < p.HodlPercentage - 2) { - return 0; + result.AddEarlyExitCheck("HODL Underperformance", $"Strategy growth ({p.GrowthPercentage:F2}%) significantly underperforms HODL ({p.HodlPercentage:F2}%) by more than 2%. Buy-and-hold would have been more profitable."); + return result; } - var baseScore = CalculateBaseScore(p); - var finalScore = ApplyProfitabilityRules(baseScore, p); + var baseScore = CalculateBaseScore(p, result); + var finalScore = ApplyProfitabilityRules(baseScore, p, result); - return double.Clamp(finalScore, 0, 100); + result.Score = double.Clamp(finalScore, 0, 100); + result.SummaryMessage = result.GenerateSummaryMessage(); + + return result; } - catch + catch (Exception ex) { - return 0; + result.AddEarlyExitCheck("Calculation Error", $"An error occurred during score calculation: {ex.Message}"); + return result; } } - private static double CalculateBaseScore(BacktestScoringParams p) + private static double CalculateBaseScore(BacktestScoringParams p, BacktestScoringResult result) { var componentScores = new Dictionary { - { "GrowthPercentage", CalculateGrowthScore(p.GrowthPercentage) }, - { "SharpeRatio", CalculateSharpeScore(p.SharpeRatio) }, - { "MaxDrawdownUsd", CalculateDrawdownUsdScore(p.MaxDrawdown, p.InitialBalance) }, - { "HodlComparison", CalculateHodlComparisonScore(p.GrowthPercentage, p.HodlPercentage) }, - { "WinRate", CalculateWinRateScore(p.WinRate, p.TradeCount) }, - { "ProfitabilityBonus", CalculateProfitabilityBonus(p.GrowthPercentage) }, - { "TradeCount", CalculateTradeCountScore(p.TradeCount) }, - { "RecoveryTime", CalculateRecoveryScore(p.MaxDrawdownRecoveryTime, p.Timeframe) }, - { "TestDuration", CalculateTestDurationScore(p.StartDate, p.EndDate, p.Timeframe) }, - { "FeesImpact", CalculateFeesImpactScore(p.FeesPaid, p.InitialBalance, (decimal)p.TotalPnL) }, - { "RiskAdjustedReturn", CalculateRiskAdjustedReturnScore(p.TotalPnL, p.MaxDrawdown, p.InitialBalance) } + { "GrowthPercentage", CalculateGrowthScore(p.GrowthPercentage, result) }, + { "SharpeRatio", CalculateSharpeScore(p.SharpeRatio, result) }, + { "MaxDrawdownUsd", CalculateDrawdownUsdScore(p.MaxDrawdown, p.InitialBalance, result) }, + { "HodlComparison", CalculateHodlComparisonScore(p.GrowthPercentage, p.HodlPercentage, result) }, + { "WinRate", CalculateWinRateScore(p.WinRate, p.TradeCount, result) }, + { "ProfitabilityBonus", CalculateProfitabilityBonus(p.GrowthPercentage, result) }, + { "TradeCount", CalculateTradeCountScore(p.TradeCount, result) }, + { "RecoveryTime", CalculateRecoveryScore(p.MaxDrawdownRecoveryTime, p.Timeframe, result) }, + { "TestDuration", CalculateTestDurationScore(p.StartDate, p.EndDate, p.Timeframe, result) }, + { "FeesImpact", CalculateFeesImpactScore(p.FeesPaid, p.InitialBalance, (decimal)p.TotalPnL, result) }, + { "RiskAdjustedReturn", CalculateRiskAdjustedReturnScore(p.TotalPnL, p.MaxDrawdown, p.InitialBalance, result) } }; - return componentScores.Sum(kvp => kvp.Value * Weights[kvp.Key]); + var totalScore = componentScores.Sum(kvp => kvp.Value * Weights[kvp.Key]); + + // Add component scores to result + foreach (var kvp in componentScores) + { + var passed = kvp.Value > 0; + result.AddCheck(kvp.Key, kvp.Value, Weights[kvp.Key], GetComponentMessage(kvp.Key, kvp.Value, p), passed); + } + + return totalScore; } - private static double ApplyProfitabilityRules(double baseScore, BacktestScoringParams p) + private static string GetComponentMessage(string component, double score, BacktestScoringParams p) + { + return component switch + { + "GrowthPercentage" => $"Growth of {p.GrowthPercentage:F2}% (target: 10% for full score)", + "SharpeRatio" => $"Sharpe ratio of {p.SharpeRatio:F2} (target: 4.0 for full score)", + "MaxDrawdownUsd" => $"Max drawdown of ${p.MaxDrawdown:F2} ({p.MaxDrawdownPc:F1}% of balance)", + "HodlComparison" => $"Strategy vs HODL: {p.GrowthPercentage:F2}% vs {p.HodlPercentage:F2}% (difference: {p.GrowthPercentage - p.HodlPercentage:F2}%)", + "WinRate" => $"Win rate of {p.WinRate * 100:F1}% with {p.TradeCount} trades", + "ProfitabilityBonus" => $"Bonus for positive growth of {p.GrowthPercentage:F2}%", + "TradeCount" => $"{p.TradeCount} trades executed (minimum 5, optimal 50+)", + "RecoveryTime" => $"Recovery time: {p.MaxDrawdownRecoveryTime.TotalDays:F1} days", + "TestDuration" => $"Test duration: {(p.EndDate - p.StartDate).TotalDays:F1} days", + "FeesImpact" => $"Fees: ${p.FeesPaid:F2} ({p.FeesPaid / p.InitialBalance * 100:F2}% of balance)", + "RiskAdjustedReturn" => $"PnL/Drawdown ratio: {p.TotalPnL / (double)p.MaxDrawdown:F2}:1", + _ => $"Component score: {score:F1}" + }; + } + + private static double ApplyProfitabilityRules(double baseScore, BacktestScoringParams p, BacktestScoringResult result) { var penaltyMultiplier = 1.0; @@ -80,30 +123,36 @@ public class BacktestScorer if (p.GrowthPercentage < 0) { var negativePenalty = Math.Abs(p.GrowthPercentage) * 0.1; // 10% penalty per 1% loss - penaltyMultiplier *= Math.Max(0.1, 1 - negativePenalty); + var newMultiplier = Math.Max(0.1, 1 - negativePenalty); + penaltyMultiplier *= newMultiplier; + result.AddPenaltyCheck("Negative Growth", newMultiplier, $"Negative growth of {p.GrowthPercentage:F2}% applied {negativePenalty:F1}% penalty"); } - // Note: Negative PnL is now handled by early exit in CalculateTotalScore - // 3. Win Rate Validation (Dynamic) if (p.WinRate < 0.3 && p.TradeCount > 10) { var winRatePenalty = (0.3 - p.WinRate) * 0.5; // 50% penalty per 10% below 30% - penaltyMultiplier *= Math.Max(0.2, 1 - winRatePenalty); + var newMultiplier = Math.Max(0.2, 1 - winRatePenalty); + penaltyMultiplier *= newMultiplier; + result.AddPenaltyCheck("Low Win Rate", newMultiplier, $"Win rate of {p.WinRate * 100:F1}% below 30% threshold applied {winRatePenalty:F1}% penalty"); } // 4. Minimum Profit Threshold (Dynamic) if (p.GrowthPercentage < 2 && p.TradeCount > 5) { var profitPenalty = (2 - p.GrowthPercentage) * 0.1; // 10% penalty per 1% below 2% - penaltyMultiplier *= Math.Max(0.5, 1 - profitPenalty); + var newMultiplier = Math.Max(0.5, 1 - profitPenalty); + penaltyMultiplier *= newMultiplier; + result.AddPenaltyCheck("Low Profit", newMultiplier, $"Growth of {p.GrowthPercentage:F2}% below 2% minimum applied {profitPenalty:F1}% penalty"); } // 5. Drawdown Penalty (Dynamic) - Enhanced to consider PnL ratio if (p.MaxDrawdownPc > 20) { var drawdownPenalty = (p.MaxDrawdownPc - 20) * 0.02; // 2% penalty per 1% above 20% - penaltyMultiplier *= Math.Max(0.3, 1 - drawdownPenalty); + var newMultiplier = Math.Max(0.3, 1 - drawdownPenalty); + penaltyMultiplier *= newMultiplier; + result.AddPenaltyCheck("High Drawdown", newMultiplier, $"Drawdown of {p.MaxDrawdownPc:F1}% above 20% threshold applied {drawdownPenalty:F1}% penalty"); } // 6. Enhanced Drawdown vs PnL Penalty @@ -113,7 +162,9 @@ public class BacktestScorer if (drawdownToPnLRatio > 1.5) // If drawdown is more than 150% of PnL { var ratioPenalty = (drawdownToPnLRatio - 1.5) * 0.2; // 20% penalty per 0.5 ratio above 1.5 - penaltyMultiplier *= Math.Max(0.2, 1 - ratioPenalty); + var newMultiplier = Math.Max(0.2, 1 - ratioPenalty); + penaltyMultiplier *= newMultiplier; + result.AddPenaltyCheck("Poor Risk/Reward", newMultiplier, $"Drawdown/PnL ratio of {drawdownToPnLRatio:F2}:1 above 1.5:1 threshold applied {ratioPenalty:F1}% penalty"); } } @@ -122,7 +173,9 @@ public class BacktestScorer if (testDurationDays < 30) { var durationPenalty = (30 - testDurationDays) * 0.02; // 2% penalty per day below 30 - penaltyMultiplier *= Math.Max(0.5, 1 - durationPenalty); + var newMultiplier = Math.Max(0.5, 1 - durationPenalty); + penaltyMultiplier *= newMultiplier; + result.AddPenaltyCheck("Short Test Duration", newMultiplier, $"Test duration of {testDurationDays:F1} days below 30-day minimum applied {durationPenalty:F1}% penalty"); } // 8. HODL Underperformance Penalty (Dynamic) @@ -130,13 +183,15 @@ public class BacktestScorer { var hodlUnderperformance = p.HodlPercentage - p.GrowthPercentage; var hodlPenalty = hodlUnderperformance * 0.3; // 30% penalty per 1% underperformance - penaltyMultiplier *= Math.Max(0.1, 1 - hodlPenalty); + var newMultiplier = Math.Max(0.1, 1 - hodlPenalty); + penaltyMultiplier *= newMultiplier; + result.AddPenaltyCheck("HODL Underperformance", newMultiplier, $"Underperforming HODL by {hodlUnderperformance:F2}% applied {hodlPenalty:F1}% penalty"); } return baseScore * penaltyMultiplier; } - private static double CalculateGrowthScore(double growthPercentage) + private static double CalculateGrowthScore(double growthPercentage, BacktestScoringResult result) { // More aggressive scoring - harder to reach 100 if (growthPercentage < 0) @@ -164,7 +219,7 @@ public class BacktestScorer }; } - private static double CalculateProfitabilityBonus(double growthPercentage) + private static double CalculateProfitabilityBonus(double growthPercentage, BacktestScoringResult result) { return growthPercentage switch { @@ -173,7 +228,7 @@ public class BacktestScorer }; } - private static double CalculateSharpeScore(double sharpeRatio) + private static double CalculateSharpeScore(double sharpeRatio, BacktestScoringResult result) { return sharpeRatio switch { @@ -183,7 +238,7 @@ public class BacktestScorer }; } - private static double CalculateDrawdownUsdScore(decimal maxDrawdown, decimal initialBalance) + private static double CalculateDrawdownUsdScore(decimal maxDrawdown, decimal initialBalance, BacktestScoringResult result) { if (initialBalance <= 0) return 0; @@ -195,7 +250,7 @@ public class BacktestScorer }; } - private static double CalculateWinRateScore(double winRate, int tradeCount) + private static double CalculateWinRateScore(double winRate, int tradeCount, BacktestScoringResult result) { // Base win rate score var baseScore = winRate * 100; @@ -212,7 +267,7 @@ public class BacktestScorer return baseScore * significanceFactor; } - private static double CalculateHodlComparisonScore(double strategyGrowth, double hodlGrowth) + private static double CalculateHodlComparisonScore(double strategyGrowth, double hodlGrowth, BacktestScoringResult result) { var difference = strategyGrowth - hodlGrowth; @@ -228,7 +283,7 @@ public class BacktestScorer }; } - private static double CalculateTradeCountScore(int tradeCount) + private static double CalculateTradeCountScore(int tradeCount, BacktestScoringResult result) { return tradeCount switch { @@ -239,7 +294,7 @@ public class BacktestScorer }; } - private static double CalculateRecoveryScore(TimeSpan recoveryTime, Timeframe timeframe) + private static double CalculateRecoveryScore(TimeSpan recoveryTime, Timeframe timeframe, BacktestScoringResult result) { var days = recoveryTime.TotalDays; @@ -260,7 +315,7 @@ public class BacktestScorer return 100 - (days / maxRecoveryDays * 100); } - private static double CalculateTestDurationScore(DateTime startDate, DateTime endDate, Timeframe timeframe) + private static double CalculateTestDurationScore(DateTime startDate, DateTime endDate, Timeframe timeframe, BacktestScoringResult result) { var durationDays = (endDate - startDate).TotalDays; @@ -283,7 +338,7 @@ public class BacktestScorer return 100; } - private static double CalculateFeesImpactScore(decimal feesPaid, decimal initialBalance, decimal totalPnL) + private static double CalculateFeesImpactScore(decimal feesPaid, decimal initialBalance, decimal totalPnL, BacktestScoringResult result) { if (initialBalance <= 0) return 0; @@ -307,7 +362,7 @@ public class BacktestScorer return feeEfficiency; } - private static double CalculateRiskAdjustedReturnScore(double totalPnL, decimal maxDrawdown, decimal initialBalance) + private static double CalculateRiskAdjustedReturnScore(double totalPnL, decimal maxDrawdown, decimal initialBalance, BacktestScoringResult result) { if (initialBalance <= 0 || maxDrawdown <= 0) return 0; diff --git a/src/Managing.Infrastructure.Database/BacktestRepository.cs b/src/Managing.Infrastructure.Database/BacktestRepository.cs index 1666514..9b02eed 100644 --- a/src/Managing.Infrastructure.Database/BacktestRepository.cs +++ b/src/Managing.Infrastructure.Database/BacktestRepository.cs @@ -157,7 +157,8 @@ public class BacktestRepository : IBacktestRepository MaxDrawdown = b.Statistics?.MaxDrawdown, Fees = b.Fees, SharpeRatio = b.Statistics?.SharpeRatio != null ? (double)b.Statistics.SharpeRatio : null, - Score = b.Score + Score = b.Score, + ScoreMessage = b.ScoreMessage ?? string.Empty }); return (mappedBacktests, (int)totalCount); @@ -284,7 +285,8 @@ public class BacktestRepository : IBacktestRepository MaxDrawdown = b.Statistics?.MaxDrawdown, Fees = b.Fees, SharpeRatio = b.Statistics?.SharpeRatio != null ? (double)b.Statistics.SharpeRatio : null, - Score = b.Score + Score = b.Score, + ScoreMessage = b.ScoreMessage ?? string.Empty }); return (mappedBacktests, (int)totalCount); diff --git a/src/Managing.Infrastructure.Database/MongoDb/Collections/BacktestDto.cs b/src/Managing.Infrastructure.Database/MongoDb/Collections/BacktestDto.cs index 1146ffa..900822a 100644 --- a/src/Managing.Infrastructure.Database/MongoDb/Collections/BacktestDto.cs +++ b/src/Managing.Infrastructure.Database/MongoDb/Collections/BacktestDto.cs @@ -29,6 +29,7 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections [BsonRepresentation(BsonType.Decimal128)] public decimal Fees { get; set; } public double Score { get; set; } + public string ScoreMessage { get; set; } = string.Empty; public string Identifier { get; set; } public string RequestId { get; set; } public string? Metadata { get; set; } diff --git a/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs b/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs index bf83d92..9ad7eab 100644 --- a/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs +++ b/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs @@ -150,6 +150,7 @@ public static class MongoMappers StartDate = b.StartDate, EndDate = b.EndDate, Score = b.Score, + ScoreMessage = b.ScoreMessage ?? string.Empty, RequestId = b.RequestId, Metadata = string.IsNullOrEmpty(b.Metadata) ? null : JsonSerializer.Deserialize(b.Metadata) }; @@ -179,6 +180,7 @@ public static class MongoMappers StartDate = result.StartDate, EndDate = result.EndDate, Score = result.Score, + ScoreMessage = result.ScoreMessage ?? string.Empty, RequestId = result.RequestId, Metadata = result.Metadata == null ? null : JsonSerializer.Serialize(result.Metadata) };