209 lines
8.7 KiB
C#
209 lines
8.7 KiB
C#
using System;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using FinlyticAnalyzer.Database;
|
|
using FinlyticAnalyzer.Entities;
|
|
using FinlyticAnalyzer.Services;
|
|
using FinlyticCore.Models.Analyzer;
|
|
using FinlyticCore.Models.Trades;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace FinlyticAnalyzer.Controllers;
|
|
|
|
public class ManualAnalysisRequest
|
|
{
|
|
public string Symbol { get; set; } = string.Empty;
|
|
public string Isin { get; set; } = string.Empty;
|
|
public string Sector { get; set; } = "Technology";
|
|
public string Headline { get; set; } = "Manual User Request";
|
|
public decimal CurrentPrice { get; set; } = 100.0m;
|
|
public int RiskScore { get; set; } = 50; // 0 to 100
|
|
public int MinTimeframeValue { get; set; } = 4;
|
|
public int MaxTimeframeValue { get; set; } = 6;
|
|
public string TimeframeUnit { get; set; } = "Tage";
|
|
public string InstrumentType { get; set; } = "Stock";
|
|
public string UserNotes { get; set; } = string.Empty;
|
|
}
|
|
|
|
[ApiController]
|
|
[Route("api/v1/analyze")]
|
|
public class ManualAnalysisController : ControllerBase
|
|
{
|
|
private readonly IVixTrackerService _vixTracker;
|
|
private readonly IN8nEvaluationService _n8nService;
|
|
private readonly IWinRateCalculator _winRateCalculator;
|
|
private readonly AnalyzerDbContext _dbContext;
|
|
private readonly ILogger<ManualAnalysisController> _logger;
|
|
|
|
public ManualAnalysisController(
|
|
IVixTrackerService vixTracker,
|
|
IN8nEvaluationService n8nService,
|
|
IWinRateCalculator winRateCalculator,
|
|
AnalyzerDbContext dbContext,
|
|
ILogger<ManualAnalysisController> logger)
|
|
{
|
|
_vixTracker = vixTracker;
|
|
_n8nService = n8nService;
|
|
_winRateCalculator = winRateCalculator;
|
|
_dbContext = dbContext;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs a manual analysis based on the provided request.
|
|
/// </summary>
|
|
[HttpPost("manual")]
|
|
public async Task<IActionResult> RunManualAnalysis([FromBody] ManualAnalysisRequest request, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(request.Symbol) && string.IsNullOrWhiteSpace(request.Isin))
|
|
{
|
|
return BadRequest(new { error = "Symbol or ISIN is required." });
|
|
}
|
|
|
|
var regime = _vixTracker.GetCurrentRegime();
|
|
var currentVix = _vixTracker.GetCurrentVix();
|
|
string analysisId = Guid.NewGuid().ToString("N");
|
|
double winRate = _winRateCalculator.CalculateWinRate(request.Sector, request.Symbol, regime);
|
|
|
|
string riskLabel = request.RiskScore > 70 ? $"Aggressiv ({request.RiskScore}/100)" : (request.RiskScore > 30 ? $"Balanced ({request.RiskScore}/100)" : $"Konservativ ({request.RiskScore}/100)");
|
|
string timeframeFormatted = $"{request.MinTimeframeValue}-{request.MaxTimeframeValue} {request.TimeframeUnit}";
|
|
|
|
var n8nRequest = new N8nAnalysisRequestDto
|
|
{
|
|
RequestId = analysisId,
|
|
Timestamp = DateTime.UtcNow,
|
|
TriggerType = "Manual",
|
|
TargetAsset = new TargetAssetInfo
|
|
{
|
|
Symbol = request.Symbol.ToUpperInvariant(),
|
|
Isin = request.Isin.ToUpperInvariant(),
|
|
Sector = request.Sector
|
|
},
|
|
MarketContext = new MarketContextInfo
|
|
{
|
|
Vix = currentVix,
|
|
MarketRegime = regime.ToString()
|
|
},
|
|
FilterContext = new FilterContextInfo
|
|
{
|
|
ImpactScore = 1.0,
|
|
RawNewsHeadline = string.IsNullOrWhiteSpace(request.Headline) ? "Manual User Trigger" : request.Headline
|
|
},
|
|
UserPreferences = new UserPreferencesInfo
|
|
{
|
|
RiskScore = request.RiskScore,
|
|
RiskTolerance = riskLabel,
|
|
MinTimeframeValue = request.MinTimeframeValue,
|
|
MaxTimeframeValue = request.MaxTimeframeValue,
|
|
TimeframeUnit = request.TimeframeUnit,
|
|
TimeframeFormatted = timeframeFormatted,
|
|
InstrumentType = request.InstrumentType,
|
|
UserNotes = request.UserNotes
|
|
},
|
|
TradeFeedback = new TradeFeedbackInfo
|
|
{
|
|
TotalAssetTrades = 0,
|
|
AssetWinRate = winRate,
|
|
AvgReturnPercent = 0.0,
|
|
LastTradeResult = "UNKNOWN"
|
|
}
|
|
};
|
|
|
|
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")[..10].ToUpperInvariant(),
|
|
AnalysisId = analysisId,
|
|
EventId = analysisId,
|
|
Sector = request.Sector,
|
|
Symbol = request.Symbol.ToUpperInvariant(),
|
|
Isin = request.Isin.ToUpperInvariant(),
|
|
CompanyName = request.Symbol,
|
|
EntryPrice = request.CurrentPrice,
|
|
SignalType = string.Equals(n8nResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "SELL" : "BUY",
|
|
RiskTolerance = n8nResponse.SuggestedRisk,
|
|
Timeframe = timeframeFormatted,
|
|
InstrumentType = request.InstrumentType,
|
|
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
|
|
};
|
|
}
|
|
|
|
var analysisEntity = new AnalysisEntity
|
|
{
|
|
AnalysisId = analysisId,
|
|
EventId = analysisId,
|
|
Sector = request.Sector,
|
|
Symbol = request.Symbol.ToUpperInvariant(),
|
|
Isin = request.Isin.ToUpperInvariant(),
|
|
VixRegime = regime,
|
|
VixValue = currentVix,
|
|
ImpactScore = 1.0,
|
|
WinRate = dynamicWinRate,
|
|
RawDataJson = JsonSerializer.Serialize(request),
|
|
AiOutputJson = proposal != null ? JsonSerializer.Serialize(proposal) : "{}",
|
|
N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}",
|
|
N8nEvalScore = n8nResponse?.EvalScore ?? 0,
|
|
N8nDecision = n8nResponse?.AiDecision ?? "Rejected",
|
|
IsTradeProposed = shouldProceed,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
_dbContext.Analyses.Add(analysisEntity);
|
|
await _dbContext.SaveChangesAsync(cancellationToken);
|
|
|
|
if (!shouldProceed)
|
|
{
|
|
return Ok(new
|
|
{
|
|
analysisId,
|
|
isTradeProposed = false,
|
|
status = "Rejected",
|
|
recommendation = "NOT_RECOMMENDED",
|
|
reasoning = n8nResponse?.AiReasoning ?? "Die KI stuft diesen Trade als zu riskant ein und empfiehlt keine Positionierung.",
|
|
n8nResponse,
|
|
proposal = (object?)null
|
|
});
|
|
}
|
|
|
|
return Ok(new
|
|
{
|
|
analysisId,
|
|
isTradeProposed = true,
|
|
status = "Success",
|
|
recommendation = "RECOMMENDED",
|
|
n8nResponse,
|
|
proposal
|
|
});
|
|
}
|
|
}
|