581 lines
26 KiB
C#
581 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using FinlyticCore.Dtos.TechnicalAnalysis;
|
|
using FinlyticCore.Services;
|
|
using FinlyticTechnicals.Database;
|
|
using FinlyticTechnicals.Entities;
|
|
using FinlyticTechnicals.Indicators;
|
|
using FinlyticTechnicals.Patterns;
|
|
using FinlyticTechnicals.Strategies;
|
|
using FinlyticTechnicals.Util;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace FinlyticTechnicals.Services;
|
|
|
|
public interface ITechnicalScoringEngine
|
|
{
|
|
/// <summary>
|
|
/// Evaluates technical setup, indicators, and patterns for an ISIN and returns trading setups.
|
|
/// </summary>
|
|
/// <param name="universeSource">
|
|
/// Which universe-selection mechanism this ISIN is currently monitored under (favorite/discovery/
|
|
/// sentiment-spike), if known - passed through onto the returned <see cref="StrategyResultDto"/>s and
|
|
/// persisted alongside them so downstream consumers (FinlyticEngine) can record why the asset was being
|
|
/// watched. <see langword="null"/> for an ad hoc analysis outside the scan universe.
|
|
/// </param>
|
|
/// <param name="universeEnteredAtUtc">When the ISIN entered that universe, alongside <paramref name="universeSource"/>.</param>
|
|
Task<List<StrategyResultDto>> AnalyzeIsinAsync(string isin, string? symbol = null, UniverseSource? universeSource = null, DateTime? universeEnteredAtUtc = null, CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Gets full technical analysis including candles, calculated indicators, patterns, and signals for an ISIN.
|
|
/// </summary>
|
|
Task<TechnicalAnalysisDto?> GetTechnicalAnalysisDtoAsync(string isin, string? symbol = null, CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Gets all active top-pick setups from the database.
|
|
/// </summary>
|
|
Task<List<StrategyResultDto>> GetActiveSetupsAsync(bool topPicksOnly = false, int limit = 50, decimal? minScore = null, CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Returns the last <paramref name="limit"/> setups persisted for <paramref name="isin"/> across all scan
|
|
/// cycles, most recent first - regardless of <c>IsActive</c>/expiry/top-pick status, so a caller can see
|
|
/// the raw quality-score trend over time, including setups too weak to ever have reached the engine.
|
|
/// </summary>
|
|
Task<List<StrategyResultDto>> GetRecentSetupHistoryAsync(string isin, int limit = 8, CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public class TechnicalScoringEngine : ITechnicalScoringEngine
|
|
{
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly IMultiTimeframeCandleAggregator _aggregator;
|
|
private readonly IYahooMarketDataScraper _yahooScraper;
|
|
private readonly IEnumerable<IPatternDetector> _patternDetectors;
|
|
private readonly IEnumerable<ITechnicalStrategy> _strategies;
|
|
private readonly IFinlyticLogger<TechnicalScoringEngine> _logger;
|
|
|
|
public TechnicalScoringEngine(
|
|
IServiceScopeFactory scopeFactory,
|
|
IMultiTimeframeCandleAggregator aggregator,
|
|
IYahooMarketDataScraper yahooScraper,
|
|
IEnumerable<IPatternDetector> patternDetectors,
|
|
IEnumerable<ITechnicalStrategy> strategies,
|
|
IFinlyticLogger<TechnicalScoringEngine> logger)
|
|
{
|
|
_scopeFactory = scopeFactory;
|
|
_aggregator = aggregator;
|
|
_yahooScraper = yahooScraper;
|
|
_patternDetectors = patternDetectors;
|
|
_strategies = strategies;
|
|
_logger = logger;
|
|
}
|
|
|
|
|
|
public async Task<List<StrategyResultDto>> AnalyzeIsinAsync(string isin, string? symbol = null, UniverseSource? universeSource = null, DateTime? universeEnteredAtUtc = null, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(isin)) return [];
|
|
var cleanIsin = isin.Trim().ToUpperInvariant();
|
|
|
|
// 1. Resolve ticker symbol if needed
|
|
string targetSymbol = symbol ?? string.Empty;
|
|
if (string.IsNullOrWhiteSpace(targetSymbol))
|
|
{
|
|
targetSymbol = await _yahooScraper.ResolveTickerFromIsinAsync(cleanIsin, cancellationToken) ?? cleanIsin;
|
|
}
|
|
|
|
// 2. Ensure historical multi-timeframe candles are available in ring buffers
|
|
var candles15m = _aggregator.GetCandles(cleanIsin, "15m");
|
|
var candles1h = _aggregator.GetCandles(cleanIsin, "1h");
|
|
var candles1d = _aggregator.GetCandles(cleanIsin, "1d");
|
|
|
|
if (candles1d.Count < 20 || candles15m.Count < 10)
|
|
{
|
|
// Backfill deep history from Yahoo
|
|
var dailyRes = await _yahooScraper.FetchHistoricalCandlesAsync(targetSymbol, "1y", "1d", cancellationToken);
|
|
if (dailyRes.Count > 0)
|
|
{
|
|
var dailyDtos = dailyRes.Select(c => new CandleDto(c.Timestamp, c.Open, c.High, c.Low, c.Close, c.Volume)).ToList();
|
|
_aggregator.InitializeHistory(cleanIsin, "1d", dailyDtos);
|
|
}
|
|
|
|
var hourlyRes = await _yahooScraper.FetchHistoricalCandlesAsync(targetSymbol, "60d", "1h", cancellationToken);
|
|
if (hourlyRes.Count > 0)
|
|
{
|
|
var hourlyDtos = hourlyRes.Select(c => new CandleDto(c.Timestamp, c.Open, c.High, c.Low, c.Close, c.Volume)).ToList();
|
|
_aggregator.InitializeHistory(cleanIsin, "1h", hourlyDtos);
|
|
}
|
|
|
|
var min15Res = await _yahooScraper.FetchHistoricalCandlesAsync(targetSymbol, "10d", "15m", cancellationToken);
|
|
if (min15Res.Count > 0)
|
|
{
|
|
var min15Dtos = min15Res.Select(c => new CandleDto(c.Timestamp, c.Open, c.High, c.Low, c.Close, c.Volume)).ToList();
|
|
_aggregator.InitializeHistory(cleanIsin, "15m", min15Dtos);
|
|
}
|
|
}
|
|
|
|
var allTimeframes = _aggregator.GetAllTimeframes(cleanIsin);
|
|
var primaryCandles = _aggregator.GetCandles(cleanIsin, "15m");
|
|
if (primaryCandles.Count == 0)
|
|
{
|
|
primaryCandles = _aggregator.GetCandles(cleanIsin, "1d");
|
|
}
|
|
|
|
if (primaryCandles.Count < 5)
|
|
{
|
|
await _logger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, "[TechnicalScoringEngine] Insufficient candles for ISIN {Isin}", cleanIsin);
|
|
return [];
|
|
}
|
|
|
|
var lastCandle = primaryCandles.Last();
|
|
decimal currentAtr = TechnicalIndicatorsEngine.CalculateAtr(primaryCandles, 14);
|
|
var adx = TechnicalIndicatorsEngine.CalculateAdx(primaryCandles, 14);
|
|
decimal ema20 = TechnicalIndicatorsEngine.CalculateEma(primaryCandles, 20);
|
|
decimal ema50 = TechnicalIndicatorsEngine.CalculateEma(primaryCandles, 50);
|
|
|
|
// Determine Market Regime
|
|
MarketRegime regime = MarketRegime.LowVolatilityRangebound;
|
|
if (adx.IsTrending)
|
|
{
|
|
regime = ema20 > ema50 ? MarketRegime.BullishTrending : MarketRegime.BearishTrending;
|
|
}
|
|
else if (currentAtr > (lastCandle.Close * 0.03m))
|
|
{
|
|
regime = MarketRegime.HighVolatilityChoppy;
|
|
}
|
|
|
|
// Build TechnicalContext
|
|
var indicators = new Dictionary<string, decimal>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["EMA_20"] = ema20,
|
|
["EMA_50"] = ema50,
|
|
["EMA_200"] = TechnicalIndicatorsEngine.CalculateEma(primaryCandles, 200),
|
|
["RSI_14"] = TechnicalIndicatorsEngine.CalculateRsi(primaryCandles, 14),
|
|
["ATR_14"] = currentAtr,
|
|
["ADX_14"] = adx.Adx,
|
|
["VWAP"] = TechnicalIndicatorsEngine.CalculateVwap(primaryCandles)
|
|
};
|
|
|
|
var context = new TechnicalContext
|
|
{
|
|
Isin = cleanIsin,
|
|
Symbol = targetSymbol,
|
|
Timeframe = "15m",
|
|
TimestampUtc = lastCandle.Timestamp,
|
|
CurrentPrice = lastCandle.Close,
|
|
CurrentSpread = 0m,
|
|
IsSpreadVolatile = false,
|
|
CurrentAtr = currentAtr,
|
|
Regime = regime,
|
|
MultiTimeframeCandles = allTimeframes,
|
|
Indicators = indicators
|
|
};
|
|
|
|
// 3. Run all Pattern Detectors
|
|
var detectedPatterns = new List<PatternResultDto>();
|
|
foreach (var detector in _patternDetectors)
|
|
{
|
|
try
|
|
{
|
|
var pat = detector.Evaluate(context);
|
|
if (pat != null)
|
|
{
|
|
detectedPatterns.Add(pat);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await _logger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[TechnicalScoringEngine] Pattern detector {Detector} threw an exception for ISIN {Isin}", detector.GetType().Name, cleanIsin);
|
|
}
|
|
}
|
|
|
|
// 4. Run all Strategies
|
|
var evaluatedSetups = new List<StrategyResultDto>();
|
|
foreach (var strategy in _strategies.OrderBy(s => s.Priority))
|
|
{
|
|
try
|
|
{
|
|
if (!strategy.IsApplicable(regime)) continue;
|
|
|
|
var setup = strategy.Evaluate(context, detectedPatterns);
|
|
if (setup != null)
|
|
{
|
|
// Confluence Scoring Calculation:
|
|
// FinalScore = 0.35 * S_ind + 0.35 * S_pattern + 0.30 * S_strat
|
|
decimal indicatorScore = CalculateIndicatorConfluenceScore(indicators, setup.Direction);
|
|
decimal patternScore = detectedPatterns.Count > 0 ? detectedPatterns.Average(p => p.QualityScore) : 50m;
|
|
decimal strategyBaseScore = setup.QualityScore;
|
|
|
|
decimal finalScore = (0.35m * indicatorScore) + (0.35m * patternScore) + (0.30m * strategyBaseScore);
|
|
finalScore = Math.Clamp(finalScore, 0m, 100m);
|
|
|
|
bool isTopPick = finalScore >= 75.0m;
|
|
string rating = finalScore >= 85.0m ? "A+" :
|
|
finalScore >= 75.0m ? "A" :
|
|
finalScore >= 60.0m ? "B" : "C";
|
|
|
|
var scoredSetup = setup with
|
|
{
|
|
QualityScore = finalScore,
|
|
IsTopPick = isTopPick,
|
|
Rating = rating,
|
|
UniverseSource = universeSource,
|
|
UniverseEnteredAtUtc = universeEnteredAtUtc,
|
|
Regime = regime
|
|
};
|
|
|
|
evaluatedSetups.Add(scoredSetup);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await _logger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[TechnicalScoringEngine] Strategy {Strategy} threw an exception for ISIN {Isin}", strategy.StrategyKey, cleanIsin);
|
|
}
|
|
}
|
|
|
|
// 5. Persist Setups and Patterns into PostgreSQL
|
|
await PersistResultsAsync(cleanIsin, targetSymbol, detectedPatterns, evaluatedSetups);
|
|
|
|
return evaluatedSetups;
|
|
}
|
|
|
|
private decimal CalculateIndicatorConfluenceScore(Dictionary<string, decimal> ind, SignalDirection dir)
|
|
{
|
|
decimal score = 50m;
|
|
if (dir == SignalDirection.Buy)
|
|
{
|
|
if (ind.TryGetValue("EMA_20", out var e20) && ind.TryGetValue("EMA_50", out var e50) && e20 > e50) score += 15m;
|
|
if (ind.TryGetValue("RSI_14", out var rsi) && rsi is >= 45m and <= 65m) score += 15m;
|
|
if (ind.TryGetValue("ADX_14", out var adx) && adx >= 25m) score += 10m;
|
|
if (ind.TryGetValue("VWAP", out var vwap) && ind.TryGetValue("EMA_20", out var e20b) && e20b > vwap) score += 10m;
|
|
}
|
|
else if (dir == SignalDirection.Sell)
|
|
{
|
|
if (ind.TryGetValue("EMA_20", out var e20) && ind.TryGetValue("EMA_50", out var e50) && e20 < e50) score += 15m;
|
|
if (ind.TryGetValue("RSI_14", out var rsi) && rsi is >= 35m and <= 55m) score += 15m;
|
|
if (ind.TryGetValue("ADX_14", out var adx) && adx >= 25m) score += 10m;
|
|
}
|
|
return Math.Clamp(score, 0m, 100m);
|
|
}
|
|
|
|
private async Task PersistResultsAsync(string isin, string symbol, List<PatternResultDto> patterns, List<StrategyResultDto> setups)
|
|
{
|
|
try
|
|
{
|
|
using var scope = _scopeFactory.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<TechnicalAnalysisDbContext>();
|
|
|
|
// Save detected patterns
|
|
foreach (var pat in patterns)
|
|
{
|
|
db.FtaDetectedPatterns.Add(new FtaDetectedPatternEntity
|
|
{
|
|
Id = pat.Id,
|
|
Isin = isin,
|
|
Timeframe = pat.Timeframe,
|
|
PatternType = pat.Type.ToString(),
|
|
Category = pat.Category.ToString(),
|
|
Bias = pat.Bias.ToString(),
|
|
Name = pat.Name,
|
|
KeyPriceLevel = pat.KeyPriceLevel,
|
|
UpperBoundary = pat.UpperBoundary,
|
|
LowerBoundary = pat.LowerBoundary,
|
|
InvalidationLevel = pat.InvalidationLevel,
|
|
QualityScore = pat.QualityScore,
|
|
Description = pat.Description,
|
|
ExtraData = pat.ExtraData,
|
|
DetectedAtUtc = pat.DetectedAt
|
|
});
|
|
}
|
|
|
|
// Save strategy setups
|
|
foreach (var setup in setups)
|
|
{
|
|
db.FtaTechnicalSetups.Add(new FtaTechnicalSetupEntity
|
|
{
|
|
SetupId = setup.SetupId,
|
|
Isin = isin,
|
|
Symbol = symbol,
|
|
Timeframe = setup.Timeframe,
|
|
StrategyKey = setup.StrategyKey,
|
|
StrategyName = setup.StrategyName,
|
|
Direction = setup.Direction.ToString(),
|
|
QualityScore = setup.QualityScore,
|
|
CurrentPrice = setup.CurrentPrice,
|
|
EntryPrice = setup.EntryPrice,
|
|
InvalidationPrice = setup.InvalidationPrice,
|
|
CurrentAtr = setup.CurrentAtr,
|
|
EstimatedRiskRewardRatio = setup.EstimatedRiskRewardRatio,
|
|
ExitPlan = setup.ExitPlan,
|
|
TechnicalRationale = setup.TechnicalRationale,
|
|
TriggeringPatterns = setup.TriggeringPatterns,
|
|
IndicatorSnapshot = setup.IndicatorSnapshot,
|
|
IsTopPick = setup.IsTopPick,
|
|
Rating = setup.Rating,
|
|
IsActive = true,
|
|
CreatedAtUtc = setup.CreatedAt,
|
|
ExpiresAtUtc = setup.ExpiresAt,
|
|
UniverseSource = setup.UniverseSource?.ToString(),
|
|
UniverseEnteredAtUtc = setup.UniverseEnteredAtUtc,
|
|
Regime = setup.Regime?.ToString()
|
|
});
|
|
}
|
|
|
|
await db.SaveChangesAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await _logger.LogWarningAsync(SettingKeys.TechnicalAnalysisChannel, ex, "[TechnicalScoringEngine] Error persisting patterns & setups for ISIN {Isin}", isin);
|
|
}
|
|
}
|
|
|
|
public async Task<List<StrategyResultDto>> GetActiveSetupsAsync(bool topPicksOnly = false, int limit = 50, decimal? minScore = null, CancellationToken cancellationToken = default)
|
|
{
|
|
using var scope = _scopeFactory.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<TechnicalAnalysisDbContext>();
|
|
|
|
var now = DateTime.UtcNow;
|
|
var query = db.FtaTechnicalSetups
|
|
.AsNoTracking()
|
|
.Where(s => s.IsActive && s.ExpiresAtUtc > now);
|
|
|
|
if (topPicksOnly)
|
|
{
|
|
query = query.Where(s => s.IsTopPick);
|
|
}
|
|
|
|
if (minScore.HasValue && minScore.Value > 0)
|
|
{
|
|
query = query.Where(s => s.QualityScore >= minScore.Value);
|
|
}
|
|
|
|
var entities = await query
|
|
.OrderByDescending(s => s.QualityScore)
|
|
.Take(limit)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
|
|
return entities.Select(e => new StrategyResultDto(
|
|
SetupId: e.SetupId,
|
|
Isin: e.Isin,
|
|
Symbol: e.Symbol,
|
|
Timeframe: e.Timeframe,
|
|
StrategyKey: e.StrategyKey,
|
|
StrategyName: e.StrategyName,
|
|
Direction: Enum.TryParse<SignalDirection>(e.Direction, out var dir) ? dir : SignalDirection.Buy,
|
|
QualityScore: e.QualityScore,
|
|
CurrentPrice: e.CurrentPrice,
|
|
EntryPrice: e.EntryPrice,
|
|
InvalidationPrice: e.InvalidationPrice,
|
|
CurrentAtr: e.CurrentAtr,
|
|
EstimatedRiskRewardRatio: e.EstimatedRiskRewardRatio,
|
|
ExitPlan: e.ExitPlan,
|
|
TechnicalRationale: e.TechnicalRationale,
|
|
TriggeringPatterns: e.TriggeringPatterns ?? [],
|
|
IndicatorSnapshot: e.IndicatorSnapshot ?? [],
|
|
CreatedAt: e.CreatedAtUtc,
|
|
ExpiresAt: e.ExpiresAtUtc,
|
|
IsTopPick: e.IsTopPick,
|
|
Rating: e.Rating,
|
|
UniverseSource: Enum.TryParse<UniverseSource>(e.UniverseSource, out var universeSource) ? universeSource : null,
|
|
UniverseEnteredAtUtc: e.UniverseEnteredAtUtc,
|
|
Regime: Enum.TryParse<MarketRegime>(e.Regime, out var regimeParsed) ? regimeParsed : null
|
|
)).ToList();
|
|
}
|
|
|
|
public async Task<List<StrategyResultDto>> GetRecentSetupHistoryAsync(string isin, int limit = 8, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(isin)) return [];
|
|
var cleanIsin = isin.Trim().ToUpperInvariant();
|
|
|
|
using var scope = _scopeFactory.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<TechnicalAnalysisDbContext>();
|
|
|
|
var entities = await db.FtaTechnicalSetups
|
|
.AsNoTracking()
|
|
.Where(s => s.Isin == cleanIsin)
|
|
.OrderByDescending(s => s.CreatedAtUtc)
|
|
.Take(limit)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
return entities.Select(e => new StrategyResultDto(
|
|
SetupId: e.SetupId,
|
|
Isin: e.Isin,
|
|
Symbol: e.Symbol,
|
|
Timeframe: e.Timeframe,
|
|
StrategyKey: e.StrategyKey,
|
|
StrategyName: e.StrategyName,
|
|
Direction: Enum.TryParse<SignalDirection>(e.Direction, out var dir) ? dir : SignalDirection.Buy,
|
|
QualityScore: e.QualityScore,
|
|
CurrentPrice: e.CurrentPrice,
|
|
EntryPrice: e.EntryPrice,
|
|
InvalidationPrice: e.InvalidationPrice,
|
|
CurrentAtr: e.CurrentAtr,
|
|
EstimatedRiskRewardRatio: e.EstimatedRiskRewardRatio,
|
|
ExitPlan: e.ExitPlan,
|
|
TechnicalRationale: e.TechnicalRationale,
|
|
TriggeringPatterns: e.TriggeringPatterns ?? [],
|
|
IndicatorSnapshot: e.IndicatorSnapshot ?? [],
|
|
CreatedAt: e.CreatedAtUtc,
|
|
ExpiresAt: e.ExpiresAtUtc,
|
|
IsTopPick: e.IsTopPick,
|
|
Rating: e.Rating,
|
|
UniverseSource: Enum.TryParse<UniverseSource>(e.UniverseSource, out var universeSource) ? universeSource : null,
|
|
UniverseEnteredAtUtc: e.UniverseEnteredAtUtc,
|
|
Regime: Enum.TryParse<MarketRegime>(e.Regime, out var regimeParsed) ? regimeParsed : null
|
|
)).ToList();
|
|
}
|
|
|
|
public async Task<TechnicalAnalysisDto?> GetTechnicalAnalysisDtoAsync(string isin, string? symbol = null, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(isin)) return null;
|
|
var cleanIsin = isin.Trim().ToUpperInvariant();
|
|
|
|
// 1. Resolve ticker symbol if needed
|
|
string targetSymbol = symbol ?? string.Empty;
|
|
if (string.IsNullOrWhiteSpace(targetSymbol))
|
|
{
|
|
targetSymbol = await _yahooScraper.ResolveTickerFromIsinAsync(cleanIsin, cancellationToken) ?? cleanIsin;
|
|
}
|
|
|
|
// 2. Ensure historical multi-timeframe candles & setups are calculated
|
|
var evaluatedSetups = await AnalyzeIsinAsync(cleanIsin, targetSymbol, cancellationToken: cancellationToken);
|
|
|
|
var candles1d = _aggregator.GetCandles(cleanIsin, "1d");
|
|
var primaryCandles = candles1d.Count > 0 ? candles1d : _aggregator.GetCandles(cleanIsin, "15m");
|
|
if (primaryCandles.Count == 0)
|
|
{
|
|
primaryCandles = _aggregator.GetCandles(cleanIsin, "1h");
|
|
}
|
|
|
|
if (primaryCandles.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var lastCandle = primaryCandles.Last();
|
|
decimal currentAtr = TechnicalIndicatorsEngine.CalculateAtr(primaryCandles, 14);
|
|
var adx = TechnicalIndicatorsEngine.CalculateAdx(primaryCandles, 14);
|
|
decimal ema20 = TechnicalIndicatorsEngine.CalculateEma(primaryCandles, 20);
|
|
decimal ema50 = TechnicalIndicatorsEngine.CalculateEma(primaryCandles, 50);
|
|
|
|
MarketRegime regime = MarketRegime.LowVolatilityRangebound;
|
|
if (adx.IsTrending)
|
|
{
|
|
regime = ema20 > ema50 ? MarketRegime.BullishTrending : MarketRegime.BearishTrending;
|
|
}
|
|
else if (currentAtr > (lastCandle.Close * 0.03m))
|
|
{
|
|
regime = MarketRegime.HighVolatilityChoppy;
|
|
}
|
|
|
|
var context = new TechnicalContext
|
|
{
|
|
Isin = cleanIsin,
|
|
Symbol = targetSymbol,
|
|
Timeframe = "1d",
|
|
TimestampUtc = lastCandle.Timestamp,
|
|
CurrentPrice = lastCandle.Close,
|
|
CurrentSpread = 0m,
|
|
IsSpreadVolatile = false,
|
|
CurrentAtr = currentAtr,
|
|
Regime = regime,
|
|
MultiTimeframeCandles = _aggregator.GetAllTimeframes(cleanIsin),
|
|
Indicators = new Dictionary<string, decimal>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["EMA_20"] = ema20,
|
|
["EMA_50"] = ema50,
|
|
["EMA_200"] = TechnicalIndicatorsEngine.CalculateEma(primaryCandles, 200),
|
|
["RSI_14"] = TechnicalIndicatorsEngine.CalculateRsi(primaryCandles, 14),
|
|
["ATR_14"] = currentAtr,
|
|
["ADX_14"] = adx.Adx,
|
|
["VWAP"] = TechnicalIndicatorsEngine.CalculateVwap(primaryCandles)
|
|
}
|
|
};
|
|
|
|
var detectedPatterns = new List<PatternResultDto>();
|
|
foreach (var detector in _patternDetectors)
|
|
{
|
|
try
|
|
{
|
|
var pat = detector.Evaluate(context);
|
|
if (pat != null)
|
|
{
|
|
detectedPatterns.Add(pat);
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
var indicatorList = new List<IndicatorValuesDto>();
|
|
var candlesList = primaryCandles.ToList();
|
|
for (int i = 0; i < candlesList.Count; i++)
|
|
{
|
|
var slice = candlesList.Take(i + 1).ToList();
|
|
var c = candlesList[i];
|
|
var macd = TechnicalIndicatorsEngine.CalculateMacd(slice);
|
|
var st = TechnicalIndicatorsEngine.CalculateSuperTrend(slice);
|
|
var atr = TechnicalIndicatorsEngine.CalculateAtr(slice, 14);
|
|
|
|
indicatorList.Add(new IndicatorValuesDto(
|
|
Timestamp: c.Timestamp,
|
|
Ema20: TechnicalIndicatorsEngine.CalculateEma(slice, 20),
|
|
Sma50: TechnicalIndicatorsEngine.CalculateSma(slice, 50),
|
|
Sma200: TechnicalIndicatorsEngine.CalculateSma(slice, 200),
|
|
Rsi14: TechnicalIndicatorsEngine.CalculateRsi(slice, 14),
|
|
MacdLine: macd.MacdLine,
|
|
MacdSignal: macd.SignalLine,
|
|
MacdHistogram: macd.Histogram,
|
|
Atr14: atr,
|
|
Vwap: TechnicalIndicatorsEngine.CalculateVwap(slice),
|
|
SupertrendUpper: st.Direction == SignalDirection.Sell ? st.Value : null,
|
|
SupertrendLower: st.Direction == SignalDirection.Buy ? st.Value : null,
|
|
SupertrendDirection: st.Direction.ToString().ToUpperInvariant(),
|
|
RecommendedStopLoss: c.Close - (atr * 2m)
|
|
));
|
|
}
|
|
|
|
var chartPatterns = detectedPatterns.Select(p => new ChartPatternDto(
|
|
Type: p.Type.ToString(),
|
|
Description: p.Description,
|
|
UpperLine: new List<PatternPointDto> { new(lastCandle.Timestamp.AddDays(-5), p.UpperBoundary > 0m ? p.UpperBoundary : lastCandle.High), new(lastCandle.Timestamp, p.UpperBoundary > 0m ? p.UpperBoundary : lastCandle.High) },
|
|
LowerLine: new List<PatternPointDto> { new(lastCandle.Timestamp.AddDays(-5), p.LowerBoundary > 0m ? p.LowerBoundary : lastCandle.Low), new(lastCandle.Timestamp, p.LowerBoundary > 0m ? p.LowerBoundary : lastCandle.Low) },
|
|
ApexTime: lastCandle.Timestamp,
|
|
BreakoutSignal: new BreakoutSignalDto(lastCandle.Timestamp, p.Bias.ToString().ToUpperInvariant(), p.KeyPriceLevel > 0m ? p.KeyPriceLevel : lastCandle.Close, p.KeyPriceLevel > 0m ? p.KeyPriceLevel * 1.05m : lastCandle.Close * 1.05m, 5.0m),
|
|
ConfidencePercent: p.QualityScore
|
|
)).ToList();
|
|
|
|
var strategySignals = evaluatedSetups.Select(s => new StrategySignalDto(
|
|
Type: s.StrategyKey,
|
|
Timestamp: s.CreatedAt,
|
|
Direction: s.Direction.ToString().ToUpperInvariant(),
|
|
Price: s.CurrentPrice,
|
|
Description: s.TechnicalRationale
|
|
)).ToList();
|
|
|
|
var marketRegimeDto = new MarketRegimeDto(
|
|
VixValue: 18.5m,
|
|
VixRegime: regime.ToString(),
|
|
MarketTrend: regime == MarketRegime.BullishTrending ? "Bullish" : regime == MarketRegime.BearishTrending ? "Bearish" : "Neutral",
|
|
DxyValue: 104.2m,
|
|
DxyState: "Neutral",
|
|
SummaryText: $"Market Regime: {regime} with ATR {currentAtr:F2}"
|
|
);
|
|
|
|
return new TechnicalAnalysisDto(
|
|
Isin: cleanIsin,
|
|
Ticker: targetSymbol,
|
|
CompanyName: targetSymbol,
|
|
LastUpdated: lastCandle.Timestamp,
|
|
Candles: candlesList,
|
|
Indicators: indicatorList,
|
|
Patterns: chartPatterns,
|
|
Signals: strategySignals,
|
|
MarketRegime: marketRegimeDto,
|
|
Currency: "EUR"
|
|
);
|
|
}
|
|
}
|