feat(trades): add live execution cockpit, closing cockpit, calculation cards and precision trade settings
This commit is contained in:
@@ -115,12 +115,19 @@ public class ManualAnalysisController : ControllerBase
|
||||
var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken);
|
||||
bool shouldProceed = n8nResponse != null && string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
double dynamicWinRate = _winRateCalculator.CalculateDynamicWinRate(
|
||||
request.Sector,
|
||||
request.Symbol,
|
||||
regime,
|
||||
n8nEvalScore: n8nResponse?.EvalScore,
|
||||
signalType: n8nResponse?.SuggestedDirection ?? "BUY");
|
||||
|
||||
TradeProposalDto? proposal = null;
|
||||
if (shouldProceed && n8nResponse != null)
|
||||
{
|
||||
proposal = new TradeProposalDto
|
||||
{
|
||||
TradeId = "PROP-" + Guid.NewGuid().ToString("N"),
|
||||
TradeId = "PROP-" + Guid.NewGuid().ToString("N")[..10].ToUpperInvariant(),
|
||||
AnalysisId = analysisId,
|
||||
EventId = analysisId,
|
||||
Sector = request.Sector,
|
||||
@@ -132,11 +139,21 @@ public class ManualAnalysisController : ControllerBase
|
||||
RiskTolerance = n8nResponse.SuggestedRisk,
|
||||
Timeframe = timeframeFormatted,
|
||||
InstrumentType = request.InstrumentType,
|
||||
WinRate = winRate,
|
||||
WinRate = dynamicWinRate,
|
||||
VixRegime = regime,
|
||||
VixValue = currentVix,
|
||||
TtlMinutes = 60,
|
||||
Reasoning = $"Manual n8n Evaluation ({n8nResponse.AiDecision}): {n8nResponse.AiReasoning}",
|
||||
StopLoss = n8nResponse.ExecutionPlan?.StopLoss ?? 0,
|
||||
TakeProfit = n8nResponse.ExecutionPlan?.TakeProfitTargets != null && n8nResponse.ExecutionPlan.TakeProfitTargets.Count > 0 ? n8nResponse.ExecutionPlan.TakeProfitTargets[0] : 0,
|
||||
EntryZoneMin = n8nResponse.ExecutionPlan?.EntryZone?.Min,
|
||||
EntryZoneMax = n8nResponse.ExecutionPlan?.EntryZone?.Max,
|
||||
TakeProfitTargets = n8nResponse.ExecutionPlan?.TakeProfitTargets,
|
||||
RiskRewardRatio = n8nResponse.ExecutionPlan?.RiskRewardRatio,
|
||||
MaxLeverage = n8nResponse.ExecutionPlan?.MaxLeverage,
|
||||
TechnicalRationale = n8nResponse.DetailedAnalysis?.TechnicalRationale ?? string.Empty,
|
||||
FundamentalRationale = n8nResponse.DetailedAnalysis?.FundamentalRationale ?? string.Empty,
|
||||
RiskWarning = n8nResponse.DetailedAnalysis?.RiskWarning ?? string.Empty,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
@@ -151,7 +168,7 @@ public class ManualAnalysisController : ControllerBase
|
||||
VixRegime = regime,
|
||||
VixValue = currentVix,
|
||||
ImpactScore = 1.0,
|
||||
WinRate = winRate,
|
||||
WinRate = dynamicWinRate,
|
||||
RawDataJson = JsonSerializer.Serialize(request),
|
||||
AiOutputJson = proposal != null ? JsonSerializer.Serialize(proposal) : "{}",
|
||||
N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}",
|
||||
|
||||
@@ -8,4 +8,18 @@ public interface IWinRateCalculator
|
||||
/// Calculates the win rate for a given sector and symbol under the specified market regime.
|
||||
/// </summary>
|
||||
double CalculateWinRate(string sector, string symbol, VixMarketRegime regime);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a multi-factor dynamic AI Win-Rate / Confidence Score using technicals, sentiment, fundamentals, AI eval score, and market regime.
|
||||
/// </summary>
|
||||
double CalculateDynamicWinRate(
|
||||
string sector,
|
||||
string symbol,
|
||||
VixMarketRegime regime,
|
||||
double? n8nEvalScore = null,
|
||||
double? technicalScore = null,
|
||||
double? sentimentScore = null,
|
||||
double? fundamentalScore = null,
|
||||
string signalType = "BUY");
|
||||
}
|
||||
|
||||
|
||||
@@ -34,31 +34,105 @@ public class WinRateCalculator : IWinRateCalculator
|
||||
/// Uses cached feedback records (3-minute TTL) to prevent disk I/O bottlenecks.
|
||||
/// </summary>
|
||||
public double CalculateWinRate(string sector, string symbol, VixMarketRegime regime)
|
||||
{
|
||||
return CalculateDynamicWinRate(sector, symbol, regime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a multi-factor dynamic AI Win-Rate / Confidence Score using technicals, sentiment, fundamentals, AI eval score, and market regime.
|
||||
/// </summary>
|
||||
public double CalculateDynamicWinRate(
|
||||
string sector,
|
||||
string symbol,
|
||||
VixMarketRegime regime,
|
||||
double? n8nEvalScore = null,
|
||||
double? technicalScore = null,
|
||||
double? sentimentScore = null,
|
||||
double? fundamentalScore = null,
|
||||
string signalType = "BUY")
|
||||
{
|
||||
try
|
||||
{
|
||||
var records = GetCachedOrLoadRecords();
|
||||
if (records.Count == 0) return 65.0;
|
||||
|
||||
var matching = records.Where(r =>
|
||||
string.Equals(r.Sector, sector, StringComparison.OrdinalIgnoreCase) &&
|
||||
r.VixRegime == regime).ToList();
|
||||
|
||||
if (matching.Count > 0)
|
||||
// 1. N8n AI Confidence Score (Weight: 40%)
|
||||
double n8nComponent = 62.0;
|
||||
if (n8nEvalScore.HasValue && n8nEvalScore.Value > 0)
|
||||
{
|
||||
int winningTrades = matching.Count(r => r.IsWin);
|
||||
double calculatedWinRate = (double)winningTrades / matching.Count * 100.0;
|
||||
_logger.LogInformation("[{Channel}] Calculated win-rate for Sector '{Sector}' in Regime '{Regime}': {WinRate:F1}% ({Wins}/{Total})",
|
||||
"AnalyzerChannel", sector, regime, calculatedWinRate, winningTrades, matching.Count);
|
||||
return Math.Round(calculatedWinRate, 1);
|
||||
n8nComponent = n8nEvalScore.Value <= 1.0 ? n8nEvalScore.Value * 100.0 : n8nEvalScore.Value;
|
||||
}
|
||||
|
||||
// 2. Technical Score (Weight: 30%)
|
||||
double taComponent = 60.0;
|
||||
if (technicalScore.HasValue && technicalScore.Value > 0)
|
||||
{
|
||||
taComponent = technicalScore.Value <= 1.0 ? technicalScore.Value * 100.0 : technicalScore.Value;
|
||||
}
|
||||
|
||||
// 3. Sentiment Score (Weight: 15%)
|
||||
double sentComponent = 58.0;
|
||||
if (sentimentScore.HasValue)
|
||||
{
|
||||
if (sentimentScore.Value >= -1.0 && sentimentScore.Value <= 1.0)
|
||||
{
|
||||
// Map sentiment from -1.0..+1.0 into 35.0..85.0
|
||||
sentComponent = 50.0 + (sentimentScore.Value * 25.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
sentComponent = sentimentScore.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fundamental Score (Weight: 15%)
|
||||
double fundComponent = 60.0;
|
||||
if (fundamentalScore.HasValue && fundamentalScore.Value > 0)
|
||||
{
|
||||
fundComponent = fundamentalScore.Value <= 1.0 ? fundamentalScore.Value * 100.0 : fundamentalScore.Value;
|
||||
}
|
||||
|
||||
// Multi-factor weighted composite
|
||||
double composite = (n8nComponent * 0.40) + (taComponent * 0.30) + (sentComponent * 0.15) + (fundComponent * 0.15);
|
||||
|
||||
// 5. Market Regime & Volatility Adjustment
|
||||
double vixAdjustment = regime switch
|
||||
{
|
||||
VixMarketRegime.LowVol => +4.0, // Calm trending market
|
||||
VixMarketRegime.Normal => +1.5, // Normal conditions
|
||||
VixMarketRegime.HighVol => -3.5, // Increased whipsaws
|
||||
VixMarketRegime.Panic => -8.0, // High panic / uncertainty
|
||||
_ => 0.0
|
||||
};
|
||||
|
||||
composite += vixAdjustment;
|
||||
|
||||
// 6. Historical track record calibration (if available in feedback records)
|
||||
var records = GetCachedOrLoadRecords();
|
||||
if (records.Count > 0)
|
||||
{
|
||||
var matching = records.Where(r =>
|
||||
string.Equals(r.Sector, sector, StringComparison.OrdinalIgnoreCase) &&
|
||||
r.VixRegime == regime).ToList();
|
||||
|
||||
if (matching.Count >= 5)
|
||||
{
|
||||
int winningTrades = matching.Count(r => r.IsWin);
|
||||
double historicalWinRate = (double)winningTrades / matching.Count * 100.0;
|
||||
composite = (composite * 0.75) + (historicalWinRate * 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp between realistic financial statistical bounds (45.0% to 92.0%)
|
||||
double finalWinRate = Math.Clamp(Math.Round(composite, 1), 45.0, 92.0);
|
||||
|
||||
_logger.LogInformation("[{Channel}] Dynamic Win-Rate for {Symbol} ({Sector}): {WinRate:F1}% [AI: {N8n:F1}%, TA: {TA:F1}%, Sent: {Sent:F1}%, Regime: {Regime}]",
|
||||
"AnalyzerChannel", symbol, sector, finalWinRate, n8nComponent, taComponent, sentComponent, regime);
|
||||
|
||||
return finalWinRate;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "[{Channel}] Error reading feedback files for win-rate calculation. Falling back to default.", "AnalyzerChannel");
|
||||
_logger.LogWarning(ex, "[{Channel}] Error calculating dynamic win-rate for {Symbol}. Fallback applied.", "AnalyzerChannel", symbol);
|
||||
return 65.0;
|
||||
}
|
||||
|
||||
return 65.0; // Default baseline win-rate
|
||||
}
|
||||
|
||||
private List<TradeFeedbackRecord> GetCachedOrLoadRecords()
|
||||
|
||||
@@ -329,11 +329,19 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
var settings = await settingsService.GetSettingsAsync();
|
||||
double minSignalScore = settings.MinSignalScore;
|
||||
|
||||
double confidenceScore = n8nResponse?.EvalScore > 0 ? n8nResponse.EvalScore : 0.75;
|
||||
double dynamicWinRate = _winRateCalculator.CalculateDynamicWinRate(
|
||||
manualReq.Sector,
|
||||
manualReq.Symbol,
|
||||
regime,
|
||||
n8nEvalScore: n8nResponse?.EvalScore,
|
||||
sentimentScore: manualReq.SentimentData?.CurrentSummary?.CompoundScore,
|
||||
signalType: n8nResponse?.SuggestedDirection ?? "BUY");
|
||||
|
||||
double confidenceScore = n8nResponse?.EvalScore > 0 ? n8nResponse.EvalScore : (dynamicWinRate / 100.0);
|
||||
bool shouldProceed = n8nResponse != null &&
|
||||
string.Equals(n8nResponse.AiDecision, "Proceed", StringComparison.OrdinalIgnoreCase) &&
|
||||
(confidenceScore * 100.0) >= minSignalScore &&
|
||||
winRate >= minSignalScore;
|
||||
dynamicWinRate >= minSignalScore;
|
||||
|
||||
TradeProposalDto? proposalDto = null;
|
||||
if (n8nResponse != null)
|
||||
@@ -353,7 +361,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
RiskTolerance = n8nResponse.SuggestedRisk,
|
||||
Timeframe = timeframeFormatted,
|
||||
InstrumentType = manualReq.InstrumentType,
|
||||
WinRate = winRate,
|
||||
WinRate = dynamicWinRate,
|
||||
VixRegime = regime,
|
||||
VixValue = currentVix,
|
||||
TtlMinutes = 60,
|
||||
@@ -384,7 +392,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
VixRegime = regime,
|
||||
VixValue = currentVix,
|
||||
ImpactScore = 1.0,
|
||||
WinRate = winRate,
|
||||
WinRate = dynamicWinRate,
|
||||
RawDataJson = JsonSerializer.Serialize(manualReq),
|
||||
AiOutputJson = proposalDto != null ? JsonSerializer.Serialize(proposalDto) : "{}",
|
||||
N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}",
|
||||
@@ -527,6 +535,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
FinlyticCore.Dtos.TechnicalAnalysis.TechnicalAnalysisDto? taResp = null;
|
||||
FinlyticCore.Dtos.Fundamentals.AssetFundamentalsDto? fundResp = null;
|
||||
FinlyticCore.Dtos.TechnicalAnalysis.LivePriceDto? livePriceResp = null;
|
||||
FinlyticCore.Dtos.Sentiment.IsinSentimentSummaryDto? sentResp = null;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -549,7 +558,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
livePriceResp = livePriceTask.Result;
|
||||
taResp = taTask.Result;
|
||||
fundResp = fundTask.Result;
|
||||
var sentResp = sentTask.Result;
|
||||
sentResp = sentTask.Result;
|
||||
|
||||
if (taResp?.Indicators != null)
|
||||
{
|
||||
@@ -735,10 +744,31 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
ActionRequired = isHighConviction ? "PROMPT_USER_FOR_MANUAL_TRADE" : "NO_ACTION"
|
||||
};
|
||||
|
||||
double dynamicWinRate = _winRateCalculator.CalculateDynamicWinRate(
|
||||
filterResult.Sector,
|
||||
finalSymbol,
|
||||
regime,
|
||||
n8nEvalScore: n8nResponse?.EvalScore,
|
||||
sentimentScore: sentResp?.CurrentSummary?.CompoundScore,
|
||||
signalType: n8nResponse?.SuggestedDirection ?? "BUY");
|
||||
|
||||
using (var scope = _scopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<AnalyzerDbContext>();
|
||||
|
||||
bool hasRecentProposal = await dbContext.Analyses.AnyAsync(a =>
|
||||
a.Isin == filterResult.Isin &&
|
||||
a.IsTradeProposed &&
|
||||
a.CreatedAt >= DateTime.UtcNow.AddHours(-4),
|
||||
cancellationToken);
|
||||
|
||||
if (hasRecentProposal && isHighConviction)
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] [AutoScreener] Asset {Symbol} ({Isin}) already has an active trade proposal in the last 4 hours. Skipping duplicate trade proposal generation.",
|
||||
"AnalyzerChannel", finalSymbol, filterResult.Isin);
|
||||
isHighConviction = false;
|
||||
}
|
||||
|
||||
var analysisEntity = new AnalysisEntity
|
||||
{
|
||||
AnalysisId = analysisId,
|
||||
@@ -749,7 +779,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
VixRegime = regime,
|
||||
VixValue = currentVix,
|
||||
ImpactScore = filterResult.ImpactScore,
|
||||
WinRate = winRate,
|
||||
WinRate = dynamicWinRate,
|
||||
RawDataJson = payloadStr,
|
||||
AiOutputJson = JsonSerializer.Serialize(recommendation),
|
||||
N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}",
|
||||
@@ -780,7 +810,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService
|
||||
RiskTolerance = n8nResponse.SuggestedRisk ?? "Balanced",
|
||||
Timeframe = $"{minTf}-{maxTf} Tage",
|
||||
InstrumentType = "KnockOut",
|
||||
WinRate = winRate,
|
||||
WinRate = dynamicWinRate,
|
||||
VixRegime = regime,
|
||||
VixValue = currentVix,
|
||||
TtlMinutes = 180,
|
||||
|
||||
Reference in New Issue
Block a user