feat(technicals,engine): add V2 multi-timeframe scoring, SMC patterns, and COS V2 engine
This commit is contained in:
@@ -34,7 +34,8 @@ builder.Services.AddSingleton<IEngineRpcClient>(sp => sp.GetRequiredService<Engi
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<EngineMqttClient>());
|
||||
|
||||
// 5. Register Engine Domain Services
|
||||
builder.Services.AddSingleton<ICompositeOpportunityScorer, CompositeOpportunityScorer>();
|
||||
// builder.Services.AddSingleton<ICompositeOpportunityScorer, CompositeOpportunityScorer>(); // V1 Fallback
|
||||
builder.Services.AddSingleton<ICompositeOpportunityScorer, CompositeOpportunityScorerV2>(); // V2 Bidirectional Active
|
||||
builder.Services.AddSingleton<IKnockOutDerivativeResolver, KnockOutDerivativeResolver>();
|
||||
builder.Services.AddSingleton<ITradeLifecycleService, TradeLifecycleService>();
|
||||
builder.Services.AddSingleton<IEvaluationHistoryService, EvaluationHistoryService>();
|
||||
|
||||
@@ -27,16 +27,17 @@ public class AiReasoningGateService : IAiReasoningGateService
|
||||
/// on a single external configuration surface.
|
||||
/// </summary>
|
||||
private const string BaseInstructions =
|
||||
"Du bist der Senior Risk & Trade Validator für Finlytic, ein automatisiertes Trading-System. " +
|
||||
"Bewerte, ob das folgende technische Setup als Trade-Vorschlag freigegeben werden soll. Prüfe " +
|
||||
"insbesondere: (1) Widersprechen sich technisches Signal, Sentiment-Lage und Fundamentaldaten? " +
|
||||
"(2) Deutet eine aktive Earnings- oder Dividenden-Sperre auf einen bevorstehenden, schwer " +
|
||||
"kalkulierbaren Kurssprung hin? (3) Was sagt die Backtest-Historie (falls vorhanden) über die " +
|
||||
"Zuverlässigkeit dieser Strategie für genau dieses Asset? (4) Passt das Risk/Reward-Verhältnis zum " +
|
||||
"aktuellen Markt-Regime? Antworte AUSSCHLIESSLICH mit einem einzelnen JSON-Objekt exakt in diesem " +
|
||||
"Schema, ohne Text davor oder danach: {\"isApproved\": bool, \"confidence\": number|null (0.0-1.0), " +
|
||||
"\"thesisSummary\": string, \"invalidationReason\": string, \"keyCatalysts\": string[], " +
|
||||
"\"identifiedRisks\": string[]}. Sei im Zweifel eher ablehnend (fail-closed) - ein verpasster Trade " +
|
||||
"Du bist der Senior Risk & Trade Validator für Finlytic, ein automatisiertes Trading-System für Long- und Short-Strategien. " +
|
||||
"Bewerte richtungsbezogen (Long/Buy oder Short/Sell), ob das folgende Setup als Trade-Vorschlag freigegeben werden soll. Prüfe " +
|
||||
"insbesondere: (1) Widersprechen sich Signal-Richtung, technisches Muster, Sentiment und Fundamentaldaten? " +
|
||||
"(Bei Long: stützen Momentum, News und Bewertung steigende Kurse? Bei Short: stützen bärische Muster, negatives Sentiment " +
|
||||
"oder schwache/überbewertete Fundamentaldaten fallende Kurse ohne extreme Squeeze-Gefahr?) " +
|
||||
"(2) Deutet eine aktive Earnings- oder Dividenden-Sperre auf einen schwer kalkulierbaren Kurssprung (Gap) gegen die Position hin? " +
|
||||
"(3) Was sagt die Backtest-Historie (falls vorhanden) über die Zuverlässigkeit dieser Strategie für dieses Asset aus? " +
|
||||
"(4) Passt das Risk/Reward-Verhältnis zum aktuellen Markt-Regime? " +
|
||||
"Antworte AUSSCHLIESSLICH mit einem einzelnen JSON-Objekt exakt in diesem Schema, ohne Text davor oder danach: " +
|
||||
"{\"isApproved\": bool, \"confidence\": number|null (0.0-1.0), \"thesisSummary\": string, \"invalidationReason\": string, " +
|
||||
"\"keyCatalysts\": string[], \"identifiedRisks\": string[]}. Sei im Zweifel eher ablehnend (fail-closed) - ein verpasster Trade " +
|
||||
"ist günstiger als ein falscher.";
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Dtos.Fundamentals;
|
||||
using FinlyticCore.Dtos.Sentiment;
|
||||
using FinlyticCore.Dtos.Simulation;
|
||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticEngine.Settings;
|
||||
|
||||
namespace FinlyticEngine.Services.Scoring;
|
||||
|
||||
/// <summary>
|
||||
/// V2 Implementation of <see cref="ICompositeOpportunityScorer"/> featuring direction-aware fundamental
|
||||
/// evaluation (Long vs Short), symmetrical sentiment scaling, and short-squeeze awareness.
|
||||
/// </summary>
|
||||
public class CompositeOpportunityScorerV2 : ICompositeOpportunityScorer
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IFinlyticLogger<CompositeOpportunityScorerV2> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CompositeOpportunityScorerV2"/> class.
|
||||
/// </summary>
|
||||
public CompositeOpportunityScorerV2(
|
||||
ISettingsService settingsService,
|
||||
IFinlyticLogger<CompositeOpportunityScorerV2> logger)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ScoringResult> CalculateCompositeScoreAsync(
|
||||
StrategyResultDto setup,
|
||||
IsinSentimentSummaryDto? sentiment,
|
||||
AssetFundamentalsDto? fundamentals,
|
||||
StrategyAssetReliabilityDto? reliability = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var wTech = await _settingsService.GetSettingAsync(EngineSettingKeys.WeightTechnical, cancellationToken);
|
||||
var wSent = await _settingsService.GetSettingAsync(EngineSettingKeys.WeightSentiment, cancellationToken);
|
||||
var wFund = await _settingsService.GetSettingAsync(EngineSettingKeys.WeightFundamental, cancellationToken);
|
||||
var lockoutDays = await _settingsService.GetSettingAsync(EngineSettingKeys.EarningsLockoutDays, cancellationToken);
|
||||
var dividendGateDays = await _settingsService.GetSettingAsync(EngineSettingKeys.DividendGateDays, cancellationToken);
|
||||
|
||||
// 1. Technical Score (0..100)
|
||||
decimal sTech = Math.Clamp(setup.QualityScore, 0m, 100m);
|
||||
|
||||
// 2. Sentiment Score (0..100) - Direction aware
|
||||
decimal sSent = 50m;
|
||||
if (sentiment?.CurrentSummary != null)
|
||||
{
|
||||
decimal compound = (decimal)sentiment.CurrentSummary.CompoundScore; // -1.0 .. +1.0
|
||||
if (setup.Direction == SignalDirection.Buy)
|
||||
{
|
||||
// Compound: -1.0 -> 0, 0.0 -> 50, +1.0 -> 100
|
||||
sSent = Math.Clamp(((compound + 1.0m) / 2.0m) * 100m, 0m, 100m);
|
||||
}
|
||||
else if (setup.Direction == SignalDirection.Sell)
|
||||
{
|
||||
// Compound: +1.0 -> 0, 0.0 -> 50, -1.0 -> 100
|
||||
sSent = Math.Clamp(((1.0m - compound) / 2.0m) * 100m, 0m, 100m);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fundamental Score (0..100) - V2 Direction Aware (Long vs Short)
|
||||
decimal sFund = 50m;
|
||||
if (fundamentals?.Fundamentals != null)
|
||||
{
|
||||
sFund = CalculateDirectionalFundamentalScore(fundamentals.Fundamentals, setup.Direction);
|
||||
}
|
||||
|
||||
// 4. Earnings Lockout Check
|
||||
int? daysToEarnings = fundamentals?.DaysToNextEarnings;
|
||||
bool passedLockout = true;
|
||||
decimal mEarnings = 1.0m;
|
||||
|
||||
if (daysToEarnings.HasValue && daysToEarnings.Value <= lockoutDays && daysToEarnings.Value >= 0)
|
||||
{
|
||||
passedLockout = false;
|
||||
mEarnings = 0.15m; // Strong suppression penalty
|
||||
await _logger.LogWarningAsync(EngineSettingKeys.ScoringChannel,
|
||||
"[CompositeScorerV2] ISIN {Isin} hit earnings lockout ({Days} days to earnings). Suppressing score.",
|
||||
setup.Isin, daysToEarnings.Value);
|
||||
}
|
||||
|
||||
// 4b. Dividend Gate Check
|
||||
int? daysToExDividend = fundamentals?.DaysToNextExDividend;
|
||||
bool passedDividendGate = true;
|
||||
decimal mDividend = 1.0m;
|
||||
|
||||
if (daysToExDividend.HasValue && daysToExDividend.Value <= dividendGateDays && daysToExDividend.Value >= 0)
|
||||
{
|
||||
passedDividendGate = false;
|
||||
mDividend = 0.5m; // Moderate suppression penalty
|
||||
await _logger.LogWarningAsync(EngineSettingKeys.ScoringChannel,
|
||||
"[CompositeScorerV2] ISIN {Isin} hit dividend gate ({Days} days to ex-dividend). Suppressing score.",
|
||||
setup.Isin, daysToExDividend.Value);
|
||||
}
|
||||
|
||||
// 5. Backtesting Matrix Feedback-Loop (Score-Bonus or Veto)
|
||||
decimal matrixBonus = 0m;
|
||||
bool passedVeto = true;
|
||||
decimal mVeto = 1.0m;
|
||||
|
||||
if (reliability != null)
|
||||
{
|
||||
if (reliability.RecommendedAction == "BOOST_SCORE" || (reliability.ProfitFactor >= 1.60m && reliability.SampleTradeCount >= 5))
|
||||
{
|
||||
matrixBonus = 15.0m;
|
||||
await _logger.LogInfoAsync(EngineSettingKeys.ScoringChannel,
|
||||
"[CompositeScorerV2] Simulation matrix bonus (+15 pts) applied for {Isin} ({Strategy}): PF={PF:F2}, WR={WR:F1}%",
|
||||
setup.Isin, setup.StrategyKey, reliability.ProfitFactor, reliability.WinRatePercent);
|
||||
}
|
||||
else if (reliability.RecommendedAction == "VETO_DISABLE" || (!reliability.IsStrategyApprovedForAsset && reliability.SampleTradeCount >= 5))
|
||||
{
|
||||
passedVeto = false;
|
||||
mVeto = 0.20m; // Heavy suppression penalty
|
||||
await _logger.LogWarningAsync(EngineSettingKeys.ScoringChannel,
|
||||
"[CompositeScorerV2] Simulation matrix VETO applied for {Isin} ({Strategy}): PF={PF:F2} < 1.00. Suppressing score.",
|
||||
setup.Isin, setup.StrategyKey, reliability.ProfitFactor);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Calculate Weighted Composite Opportunity Score (COS)
|
||||
decimal rawScore = (wTech * sTech) + (wSent * sSent) + (wFund * sFund) + matrixBonus;
|
||||
decimal finalCos = Math.Clamp(rawScore * mEarnings * mDividend * mVeto, 0m, 100m);
|
||||
|
||||
await _logger.LogInfoAsync(EngineSettingKeys.ScoringChannel,
|
||||
"[CompositeScorerV2] ISIN {Isin} ({Direction}) evaluated: COS={Cos:F1} (Tech={Tech:F1}, Sent={Sent:F1}, Fund={Fund:F1}, Bonus={Bonus}, Veto={Veto}, Lockout={Lockout}, DividendGate={DividendGate})",
|
||||
setup.Isin, setup.Direction, finalCos, sTech, sSent, sFund, matrixBonus, passedVeto, passedLockout, passedDividendGate);
|
||||
|
||||
return new ScoringResult(
|
||||
CompositeScore: Math.Round(finalCos, 2),
|
||||
TechnicalScore: Math.Round(sTech, 2),
|
||||
SentimentScore: Math.Round(sSent, 2),
|
||||
FundamentalScore: Math.Round(sFund, 2),
|
||||
PassedEarningsLockout: passedLockout,
|
||||
DaysToNextEarnings: daysToEarnings,
|
||||
ReliabilityBonus: matrixBonus,
|
||||
PassedSimulationVeto: passedVeto,
|
||||
PassedDividendGate: passedDividendGate,
|
||||
DaysToNextExDividend: daysToExDividend
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes directional fundamental score tailored specifically for Buy vs Sell opportunities.
|
||||
/// </summary>
|
||||
private static decimal CalculateDirectionalFundamentalScore(FundamentalDataDto fund, SignalDirection direction)
|
||||
{
|
||||
decimal baseScore = 50m;
|
||||
|
||||
if (direction == SignalDirection.Buy)
|
||||
{
|
||||
// Forward P/E: Low valuation supports Long (+10), extreme overvaluation penalizes (-10)
|
||||
if (fund.ForwardPe.HasValue)
|
||||
{
|
||||
if (fund.ForwardPe.Value > 0 && fund.ForwardPe.Value < 20m) baseScore += 10m;
|
||||
else if (fund.ForwardPe.Value > 45m || fund.ForwardPe.Value <= 0) baseScore -= 10m;
|
||||
}
|
||||
|
||||
// Return on Equity: Profitable return on equity supports Long (+10), capital destruction penalizes (-15)
|
||||
if (fund.ReturnOnEquity.HasValue)
|
||||
{
|
||||
if (fund.ReturnOnEquity.Value > 0.15m) baseScore += 10m;
|
||||
else if (fund.ReturnOnEquity.Value < 0.0m) baseScore -= 15m;
|
||||
}
|
||||
|
||||
// Analyst Consensus
|
||||
if (!string.IsNullOrWhiteSpace(fund.ConsensusRating))
|
||||
{
|
||||
var r = fund.ConsensusRating.ToLowerInvariant();
|
||||
if (r.Contains("buy") || r.Contains("strong_buy") || r.Contains("outperform")) baseScore += 10m;
|
||||
else if (r.Contains("sell") || r.Contains("underperform")) baseScore -= 15m;
|
||||
}
|
||||
|
||||
// Debt to Equity penalty for highly leveraged balance sheets on Longs
|
||||
if (fund.DebtToEquity.HasValue && fund.DebtToEquity.Value > 2.5m)
|
||||
{
|
||||
baseScore -= 10m;
|
||||
}
|
||||
}
|
||||
else if (direction == SignalDirection.Sell)
|
||||
{
|
||||
// Symmetrical Short evaluation:
|
||||
// Forward P/E: Extreme valuation or negative earnings supports Short (+12), deep value penalizes (-12)
|
||||
if (fund.ForwardPe.HasValue)
|
||||
{
|
||||
if (fund.ForwardPe.Value > 45m || fund.ForwardPe.Value <= 0) baseScore += 12m;
|
||||
else if (fund.ForwardPe.Value > 0 && fund.ForwardPe.Value < 15m) baseScore -= 12m;
|
||||
}
|
||||
|
||||
// Return on Equity: Capital destruction / losses supports Short (+15), high cash cow returns penalizes (-12)
|
||||
if (fund.ReturnOnEquity.HasValue)
|
||||
{
|
||||
if (fund.ReturnOnEquity.Value < 0.0m) baseScore += 15m;
|
||||
else if (fund.ReturnOnEquity.Value > 0.25m) baseScore -= 12m;
|
||||
}
|
||||
|
||||
// Analyst Consensus: Downgrades and Sell ratings confirm Short (+15), Strong Buy opposes Short (-15)
|
||||
if (!string.IsNullOrWhiteSpace(fund.ConsensusRating))
|
||||
{
|
||||
var r = fund.ConsensusRating.ToLowerInvariant();
|
||||
if (r.Contains("sell") || r.Contains("underperform") || r.Contains("downgrade")) baseScore += 15m;
|
||||
else if (r.Contains("strong_buy") || r.Contains("outperform")) baseScore -= 15m;
|
||||
}
|
||||
|
||||
// High Debt to Equity adds vulnerability in downtrend (+10)
|
||||
if (fund.DebtToEquity.HasValue && fund.DebtToEquity.Value > 2.5m)
|
||||
{
|
||||
baseScore += 10m;
|
||||
}
|
||||
|
||||
// Short Interest Float check: moderate short interest (5-15%) confirms short thesis (+5),
|
||||
// but extreme short interest (>25%) warns of dangerous short squeeze risk (-10)
|
||||
if (fund.ShortPercentOfFloat.HasValue)
|
||||
{
|
||||
if (fund.ShortPercentOfFloat.Value is >= 0.05m and <= 0.15m) baseScore += 5m;
|
||||
else if (fund.ShortPercentOfFloat.Value > 0.25m) baseScore -= 10m;
|
||||
}
|
||||
}
|
||||
|
||||
return Math.Clamp(baseScore, 0m, 100m);
|
||||
}
|
||||
}
|
||||
@@ -221,6 +221,7 @@ public class ActiveTradeMonitoringBackgroundService : BackgroundService
|
||||
return new ActiveTradeDto(
|
||||
TradeId: e.Id,
|
||||
ProposalId: e.ProposalId,
|
||||
UserId: e.UserId,
|
||||
UnderlyingIsin: e.UnderlyingIsin,
|
||||
Symbol: e.Symbol,
|
||||
DerivativeIsin: e.DerivativeIsin,
|
||||
|
||||
@@ -889,6 +889,7 @@ public class TradeLifecycleService : ITradeLifecycleService
|
||||
return new ActiveTradeDto(
|
||||
TradeId: e.Id,
|
||||
ProposalId: e.ProposalId,
|
||||
UserId: e.UserId,
|
||||
UnderlyingIsin: e.UnderlyingIsin,
|
||||
Symbol: e.Symbol,
|
||||
DerivativeIsin: e.DerivativeIsin,
|
||||
|
||||
Reference in New Issue
Block a user