feat(core): add shared DTOs, MqttTopics constants, DatabaseBootstrapper, and ManagedMqttClient extensions

This commit is contained in:
2026-08-24 21:35:24 +02:00
parent 6ab84fe1de
commit 44b161d509
39 changed files with 2545 additions and 709 deletions
@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.TechnicalAnalysis;
/// <summary>
/// Individual take-profit tier in a staged scale-out exit plan.
/// </summary>
public record TakeProfitStage(
[property: JsonPropertyName("stageNumber")] int StageNumber,
[property: JsonPropertyName("targetPrice")] decimal TargetPrice,
[property: JsonPropertyName("percentToClose")] decimal PercentToClose,
[property: JsonPropertyName("rMultiple")] decimal RMultiple,
[property: JsonPropertyName("description")] string Description
);
/// <summary>
/// Break-even trigger rule for locking in free-rolls.
/// </summary>
public record BreakEvenRule(
[property: JsonPropertyName("enabled")] bool Enabled,
[property: JsonPropertyName("triggerPrice")] decimal TriggerPrice,
[property: JsonPropertyName("offsetToCoverFees")] decimal OffsetToCoverFees
);
/// <summary>
/// Trailing stop management rule for trend following.
/// </summary>
public record TrailingStopRule(
[property: JsonPropertyName("type")] TrailingStopType Type,
[property: JsonPropertyName("multiplier")] decimal Multiplier,
[property: JsonPropertyName("activationPrice")] decimal ActivationPrice,
[property: JsonPropertyName("indicatorKey")] string IndicatorKey
);
/// <summary>
/// Indicator or structural reversal condition that triggers an early trade exit.
/// </summary>
public record ReversalCondition(
[property: JsonPropertyName("ruleDescription")] string RuleDescription,
[property: JsonPropertyName("indicatorTrigger")] string IndicatorTrigger
);
/// <summary>
/// Composable, complete exit plan decoupling entry strategy logic from execution management.
/// </summary>
public record ExitPlan(
[property: JsonPropertyName("strategyType")] ExitStrategyType StrategyType,
[property: JsonPropertyName("initialStopLoss")] decimal InitialStopLoss,
[property: JsonPropertyName("takeProfitStages")] List<TakeProfitStage> TakeProfitStages,
[property: JsonPropertyName("breakEvenRule")] BreakEvenRule? BreakEvenRule = null,
[property: JsonPropertyName("trailingStopRule")] TrailingStopRule? TrailingStopRule = null,
[property: JsonPropertyName("reversalCondition")] ReversalCondition? ReversalCondition = null,
[property: JsonPropertyName("maxHoldingBars")] int? MaxHoldingBars = null
);
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.TechnicalAnalysis;
/// <summary>
/// Output result of an isolated pattern detection evaluation.
/// </summary>
public record PatternResultDto(
[property: JsonPropertyName("id")] Guid Id,
[property: JsonPropertyName("type")] PatternType Type,
[property: JsonPropertyName("category")] PatternCategory Category,
[property: JsonPropertyName("bias")] PatternBias Bias,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("timeframe")] string Timeframe,
[property: JsonPropertyName("detectedAt")] DateTime DetectedAt,
[property: JsonPropertyName("keyPriceLevel")] decimal KeyPriceLevel,
[property: JsonPropertyName("upperBoundary")] decimal UpperBoundary,
[property: JsonPropertyName("lowerBoundary")] decimal LowerBoundary,
[property: JsonPropertyName("invalidationLevel")] decimal InvalidationLevel,
[property: JsonPropertyName("qualityScore")] decimal QualityScore,
[property: JsonPropertyName("description")] string Description,
[property: JsonPropertyName("extraData")] Dictionary<string, object>? ExtraData = null
);
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.TechnicalAnalysis;
/// <summary>
/// Fully evaluated technical trading setup output from an ITechnicalStrategy.
/// </summary>
/// <param name="UniverseSource">
/// Which FinlyticTechnicals universe-selection mechanism this ISIN was being monitored under at analysis time
/// (favorite/discovery/sentiment-spike), or <see langword="null"/> if it was analyzed ad hoc (e.g. a manual
/// "Analyze now" call for an ISIN not currently in the scan universe). Carried through unchanged onto
/// <c>EngineEvaluationSnapshotEntity</c> so the admin "why no proposals" Web UI can show not just an
/// evaluation's scores but why the asset was being watched in the first place.
/// </param>
/// <param name="UniverseEnteredAtUtc">When the ISIN above entered that scan universe, alongside <paramref name="UniverseSource"/>.</param>
/// <param name="Regime">
/// The overall market/asset technical regime (<see cref="TechnicalContext.Regime"/>) at analysis time - e.g.
/// whether this setup fired during a strong trend or a choppy/rangebound market. Forwarded onto the AI
/// validation payload (<c>AiReasoningGateService</c>) so the model has the same regime context a human trader
/// would use to judge whether a breakout is likely to follow through.
/// </param>
public record StrategyResultDto(
[property: JsonPropertyName("setupId")] Guid SetupId,
[property: JsonPropertyName("isin")] string Isin,
[property: JsonPropertyName("symbol")] string Symbol,
[property: JsonPropertyName("timeframe")] string Timeframe,
[property: JsonPropertyName("strategyKey")] string StrategyKey,
[property: JsonPropertyName("strategyName")] string StrategyName,
[property: JsonPropertyName("direction")] SignalDirection Direction,
[property: JsonPropertyName("qualityScore")] decimal QualityScore,
[property: JsonPropertyName("currentPrice")] decimal CurrentPrice,
[property: JsonPropertyName("entryPrice")] decimal EntryPrice,
[property: JsonPropertyName("invalidationPrice")] decimal InvalidationPrice,
[property: JsonPropertyName("currentAtr")] decimal CurrentAtr,
[property: JsonPropertyName("estimatedRiskRewardRatio")] decimal EstimatedRiskRewardRatio,
[property: JsonPropertyName("exitPlan")] ExitPlan ExitPlan,
[property: JsonPropertyName("technicalRationale")] string TechnicalRationale,
[property: JsonPropertyName("triggeringPatterns")] List<PatternResultDto> TriggeringPatterns,
[property: JsonPropertyName("indicatorSnapshot")] Dictionary<string, decimal> IndicatorSnapshot,
[property: JsonPropertyName("createdAt")] DateTime CreatedAt,
[property: JsonPropertyName("expiresAt")] DateTime ExpiresAt,
[property: JsonPropertyName("isTopPick")] bool IsTopPick = false,
[property: JsonPropertyName("rating")] string Rating = "B",
[property: JsonPropertyName("universeSource")] UniverseSource? UniverseSource = null,
[property: JsonPropertyName("universeEnteredAtUtc")] DateTime? UniverseEnteredAtUtc = null,
[property: JsonPropertyName("regime")] MarketRegime? Regime = null
);
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
namespace FinlyticCore.Dtos.TechnicalAnalysis;
/// <summary>
/// Execution context supplied to pattern detectors and strategy evaluators containing multi-timeframe candles and indicators.
/// </summary>
public class TechnicalContext
{
public string Isin { get; init; } = string.Empty;
public string Symbol { get; init; } = string.Empty;
public string Timeframe { get; init; } = "15m";
public DateTime TimestampUtc { get; init; } = DateTime.UtcNow;
public decimal CurrentPrice { get; init; }
public decimal CurrentSpread { get; init; }
public bool IsSpreadVolatile { get; init; }
public decimal CurrentAtr { get; init; }
public MarketRegime Regime { get; init; } = MarketRegime.LowVolatilityRangebound;
/// <summary>
/// Multi-timeframe historical candles (e.g. "1m", "5m", "15m", "1h", "1d").
/// </summary>
public Dictionary<string, IReadOnlyList<CandleDto>> MultiTimeframeCandles { get; init; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Pre-calculated mathematical indicator values for the primary timeframe.
/// </summary>
public Dictionary<string, decimal> Indicators { get; init; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Per-run overrides for a strategy's tunable indicator parameters (e.g. <c>"MeanReversion.RsiOversold"</c>),
/// keyed by <c>"{StrategyKey}.{ParameterName}"</c> so a single context could in principle carry overrides
/// for more than one strategy without name collisions. Always empty for live scanning
/// (<c>TechnicalScoringEngine</c> never populates this - Rules.md §4: no silent behavior change to live
/// trade generation as a side effect of a backtesting feature); populated only by
/// <c>FinlyticSimulation.Engine.HistoricalReplayRunner</c> from <c>BacktestRequestDto.StrategyParameters</c>,
/// so per-asset/per-strategy tuning is opt-in and scoped to backtesting. See <see cref="GetParameter"/>.
/// </summary>
public Dictionary<string, decimal> ParameterOverrides { get; init; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Resolves a tunable strategy parameter: the override in <see cref="ParameterOverrides"/> under
/// <c>"{strategyKey}.{parameterName}"</c> if present, otherwise <paramref name="defaultValue"/> (the
/// strategy's own hardcoded default, unchanged from before parametrization existed).
/// </summary>
public decimal GetParameter(string strategyKey, string parameterName, decimal defaultValue)
{
return ParameterOverrides.TryGetValue($"{strategyKey}.{parameterName}", out var v) ? v : defaultValue;
}
/// <summary>
/// Gets the candles for a specific timeframe (defaults to empty list if not found).
/// </summary>
public IReadOnlyList<CandleDto> GetCandles(string timeframe)
{
if (MultiTimeframeCandles.TryGetValue(timeframe, out var list))
{
return list;
}
return [];
}
/// <summary>
/// Gets the primary timeframe candle sequence.
/// </summary>
public IReadOnlyList<CandleDto> PrimaryCandles => GetCandles(Timeframe);
/// <summary>
/// Gets a specific indicator value or null if not computed.
/// </summary>
public decimal? GetIndicator(string key)
{
if (Indicators.TryGetValue(key, out var val))
{
return val;
}
return null;
}
}
@@ -0,0 +1,124 @@
using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos.TechnicalAnalysis;
/// <summary>
/// Major category of a chart pattern.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<PatternCategory>))]
public enum PatternCategory
{
Candlestick,
Chart,
SmartMoney
}
/// <summary>
/// Directional bias indicated by a pattern or technical setup.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<PatternBias>))]
public enum PatternBias
{
Bullish,
Bearish,
Neutral
}
/// <summary>
/// Specific pattern type recognized by pattern detection engines.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<PatternType>))]
public enum PatternType
{
// Candlestick Patterns
Hammer,
ShootingStar,
BullishEngulfing,
BearishEngulfing,
MorningStar,
EveningStar,
Doji,
// Classical Chart Patterns
DoubleBottom,
DoubleTop,
HeadAndShoulders,
InverseHeadAndShoulders,
AscendingTriangle,
DescendingTriangle,
// Smart Money Concepts (SMC)
FairValueGapBullish,
FairValueGapBearish,
LiquiditySweepHigh,
LiquiditySweepLow,
BreakOfStructure,
ChangeOfCharacter,
OrderBlock
}
/// <summary>
/// Strategy exit model defining how positions are closed or trailed.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<ExitStrategyType>))]
public enum ExitStrategyType
{
StagedScaleOutWithBreakEven,
PureTrailingStop,
DynamicBandTouch,
FixedSingleTarget,
IndicatorReversal
}
/// <summary>
/// Type of trailing stop mechanic.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<TrailingStopType>))]
public enum TrailingStopType
{
AtrMultiplier,
SuperTrendLine,
SwingPoints
}
/// <summary>
/// Direction of a technical trading setup signal.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<SignalDirection>))]
public enum SignalDirection
{
Buy,
Sell,
Neutral
}
/// <summary>
/// Overall market or asset technical regime.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<MarketRegime>))]
public enum MarketRegime
{
BullishTrending,
BearishTrending,
HighVolatilityChoppy,
LowVolatilityRangebound
}
/// <summary>
/// Which recurring FinlyticTechnicals selection mechanism added an ISIN to the continuously-scanned universe
/// (<c>TechnicalUniverseManager</c> in FinlyticTechnicals). Defined here rather than in FinlyticTechnicals
/// because it is carried on <see cref="StrategyResultDto.UniverseSource"/> across the MQTT boundary into
/// FinlyticEngine's evaluation snapshot, so more than one service needs it (Rules.md §3).
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<UniverseSource>))]
public enum UniverseSource
{
/// <summary>Promoted temporarily because FinlyticSentiment reported a strong/shifting sentiment reading.</summary>
SentimentSpike = 1,
/// <summary>Favorited by at least one user, aggregated across all users via FinlyticBackend.</summary>
UserFavorite = 2,
/// <summary>Part of FinlyticAssets' curated discovery/watchlist asset set.</summary>
Discovery = 3
}
@@ -0,0 +1,26 @@
using System;
namespace FinlyticCore.Dtos.TechnicalAnalysis;
/// <summary>
/// A single entry of FinlyticTechnicals' currently monitored scan universe ("watchlist") - the DB-backed set
/// of assets <c>TechnicalScannerBackgroundService</c> actually evaluates every cycle. Exposed to the admin web
/// UI so it's possible to verify assets are actually being watched, rather than only inferring it indirectly
/// from downstream evaluation results.
/// </summary>
public record WatchlistEntryDto(
string Isin,
string? Symbol,
string Source,
int Priority,
DateTime AddedAtUtc,
DateTime? ExpiresAtUtc
);
/// <summary>
/// Requests the last <paramref name="Limit"/> technical-analysis setups computed for <paramref name="Isin"/>,
/// most recent first, regardless of whether they were active/top-pick at the time - i.e. the raw scoring
/// history (including setups the engine's opportunity poller would have rejected as too weak), so a caller can
/// see whether an asset's quality score is trending up or down across recent scan cycles.
/// </summary>
public record GetRecentSetupHistoryRequest(string Isin, int Limit = 8);