feat(core): add shared DTOs, MqttTopics constants, DatabaseBootstrapper, and ManagedMqttClient extensions
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||
using FinlyticCore.Dtos.Trading;
|
||||
|
||||
namespace FinlyticCore.Dtos.Bot;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<BotExecutionVenue>))]
|
||||
public enum BotExecutionVenue
|
||||
{
|
||||
AlpacaPaperTrading, // Offizielle Alpaca API (US-Equities / ETFs)
|
||||
SyntheticPaperBroker // Interner Engine-Broker (EU / Knock-Outs)
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<BotPositionStatus>))]
|
||||
public enum BotPositionStatus
|
||||
{
|
||||
Pending,
|
||||
Active,
|
||||
BreakEvenTriggered,
|
||||
Tp1Hit,
|
||||
Tp2Hit,
|
||||
Closed,
|
||||
StoppedOut,
|
||||
KnockedOut,
|
||||
Canceled
|
||||
}
|
||||
|
||||
public record BotTradeOrderDto(
|
||||
Guid OrderId,
|
||||
Guid ProposalId,
|
||||
string Isin,
|
||||
string Symbol,
|
||||
BotExecutionVenue Venue,
|
||||
string? AlpacaOrderId,
|
||||
string? ClientOrderId,
|
||||
SignalDirection Direction,
|
||||
decimal RequestedQuantity,
|
||||
decimal FilledQuantity,
|
||||
decimal EntryPrice,
|
||||
decimal AverageBuyIn,
|
||||
decimal InitialStopLoss,
|
||||
decimal CurrentStopLoss,
|
||||
decimal TakeProfit1,
|
||||
decimal TakeProfit2,
|
||||
decimal CurrentPrice,
|
||||
decimal UnrealizedPnlEur,
|
||||
decimal RealizedPnlEur,
|
||||
BotPositionStatus Status,
|
||||
ExitPlan ExitPlan,
|
||||
DateTime CreatedAtUtc,
|
||||
DateTime? FilledAtUtc,
|
||||
DateTime? ClosedAtUtc
|
||||
);
|
||||
|
||||
public record AccountSummaryDto(
|
||||
decimal Equity,
|
||||
decimal Cash,
|
||||
decimal BuyingPower,
|
||||
string Currency,
|
||||
string Status
|
||||
);
|
||||
|
||||
public record BotStatusDto(
|
||||
bool IsRunning,
|
||||
bool AutoExecutionEnabled,
|
||||
int ActivePositionsCount,
|
||||
int MaxPositions,
|
||||
decimal RiskPerTradePercent,
|
||||
int MinCompositeScore,
|
||||
string VenuesActive
|
||||
);
|
||||
|
||||
public record ExecuteProposalRequest(
|
||||
Guid ProposalId,
|
||||
BotExecutionVenue? PreferredVenue = null,
|
||||
decimal? CustomQuantity = null
|
||||
);
|
||||
|
||||
public record BotPortfolioSnapshotDto(
|
||||
Guid Id,
|
||||
DateTime SnapshotDateUtc,
|
||||
decimal TotalEquityEur,
|
||||
decimal CashEur,
|
||||
int OpenPositionsCount,
|
||||
decimal DailyRealizedPnlEur,
|
||||
decimal TotalUnrealizedPnlEur,
|
||||
decimal? WinRatePercent
|
||||
);
|
||||
|
||||
public record UpdateBotSettingsRequest(
|
||||
bool? AutoExecutionEnabled,
|
||||
int? MaxPositions,
|
||||
decimal? RiskPerTradePercent,
|
||||
int? MinCompositeScore
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Result of an emergency "panic close" of every open paper-trading position (see
|
||||
/// <see cref="FinlyticCore.Util.MqttTopics.Channels.BotPanicClose"/>). <see cref="SkippedCount"/> is
|
||||
/// non-zero whenever an Alpaca position could not be liquidated (Alpaca not configured or the broker call
|
||||
/// failed) — callers MUST surface that count to the user instead of only reporting <see cref="ClosedCount"/>
|
||||
/// as if the whole operation succeeded (Rules.md §4: no fabricated full success on a partial result).
|
||||
/// </summary>
|
||||
public record PanicCloseResultDto(
|
||||
int ClosedCount,
|
||||
int SkippedCount,
|
||||
List<BotTradeOrderDto> ClosedOrders
|
||||
);
|
||||
@@ -39,4 +39,28 @@ public record AssetFundamentalsDto
|
||||
/// </summary>
|
||||
[JsonPropertyName("lastUpdatedAt")]
|
||||
public DateTime LastUpdatedAt { get; init; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Berechnete Tage bis zum nächsten Quartalszahlen-Termin (Earnings Lockout Check).
|
||||
/// </summary>
|
||||
[JsonPropertyName("daysToNextEarnings")]
|
||||
public int? DaysToNextEarnings => Events?
|
||||
.Where(e => (e.Type.Equals("Earnings", StringComparison.OrdinalIgnoreCase) || e.EventType.Equals("Earnings", StringComparison.OrdinalIgnoreCase)) && e.Date >= DateTime.UtcNow.Date)
|
||||
.OrderBy(e => e.Date)
|
||||
.Select(e => (int?)(e.Date.Date - DateTime.UtcNow.Date).TotalDays)
|
||||
.FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// Berechnete Tage bis zum nächsten Ex-Dividenden-Tag (Dividend Gate Check). Nur Events mit dem
|
||||
/// kanonischen Type "Dividend" zählen - dieser wird ausschließlich aus Trade Republics strukturierten
|
||||
/// Dividend-Feldern (ExpectedDividend/Dividends, echtes ExDate) befüllt, nicht aus dem generischen
|
||||
/// Events/PastEvents-Feed, dessen freie Type/Title-Strings nicht zuverlässig auf "Dividende" gemappt werden
|
||||
/// können (Rules.md §4: kein Raten anhand unsicherer Freitext-Strings).
|
||||
/// </summary>
|
||||
[JsonPropertyName("daysToNextExDividend")]
|
||||
public int? DaysToNextExDividend => Events?
|
||||
.Where(e => e.Type.Equals("Dividend", StringComparison.OrdinalIgnoreCase) && e.Date >= DateTime.UtcNow.Date)
|
||||
.OrderBy(e => e.Date)
|
||||
.Select(e => (int?)(e.Date.Date - DateTime.UtcNow.Date).TotalDays)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
@@ -60,6 +60,20 @@ public record ArticleRequest(
|
||||
[property: JsonPropertyName("id")] string? Id = null
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for fetching sentiment by ISIN.
|
||||
/// </summary>
|
||||
public record GetSentimentByIsinRequest(
|
||||
[property: JsonPropertyName("isin")] string Isin
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for fetching sentiment by Sector.
|
||||
/// </summary>
|
||||
public record GetSectorSentimentRequest(
|
||||
[property: JsonPropertyName("sector")] string Sector
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for triggering a manual sentiment analysis for an article or ISIN.
|
||||
/// </summary>
|
||||
@@ -84,36 +98,6 @@ public record GetEventsByMonthRequest(
|
||||
[property: JsonPropertyName("month")] int Month
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for triggering a manual AI analysis.
|
||||
/// </summary>
|
||||
public record ManualAnalysisRpcRequest(
|
||||
[property: JsonPropertyName("isin")] string Isin,
|
||||
[property: JsonPropertyName("symbol")] string Symbol,
|
||||
[property: JsonPropertyName("sector")] string Sector,
|
||||
[property: JsonPropertyName("headline")]
|
||||
string Headline,
|
||||
[property: JsonPropertyName("currentPrice")]
|
||||
decimal CurrentPrice,
|
||||
[property: JsonPropertyName("riskScore")]
|
||||
int RiskScore,
|
||||
[property: JsonPropertyName("minTimeframeValue")]
|
||||
int MinTimeframeValue,
|
||||
[property: JsonPropertyName("maxTimeframeValue")]
|
||||
int MaxTimeframeValue,
|
||||
[property: JsonPropertyName("timeframeUnit")]
|
||||
string TimeframeUnit,
|
||||
[property: JsonPropertyName("instrumentType")]
|
||||
string InstrumentType,
|
||||
[property: JsonPropertyName("userNotes")]
|
||||
string UserNotes,
|
||||
[property: JsonPropertyName("taData")] FinlyticCore.Dtos.TechnicalAnalysis.TechnicalAnalysisDto? TaData,
|
||||
[property: JsonPropertyName("fundamentalsData")]
|
||||
FinlyticCore.Dtos.Fundamentals.AssetFundamentalsDto? FundamentalsData,
|
||||
[property: JsonPropertyName("sentimentData")]
|
||||
FinlyticCore.Dtos.Sentiment.IsinSentimentSummaryDto? SentimentData
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Response payload returned by microservice health pings over MQTT.
|
||||
/// </summary>
|
||||
@@ -137,22 +121,176 @@ public record FetchLogoResponse(
|
||||
bool Success
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Payload published to MQTT when the Admin Panel updates a microservice's configuration.
|
||||
/// Replaces the anonymous type to be compatible with AOT/source-gen JSON serialization.
|
||||
/// </summary>
|
||||
public record ServiceConfigUpdatePayload(
|
||||
[property: JsonPropertyName("serviceName")]
|
||||
string ServiceName,
|
||||
[property: JsonPropertyName("timestamp")]
|
||||
DateTime Timestamp,
|
||||
[property: JsonPropertyName("settings")]
|
||||
Dictionary<string, string> Settings
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Payload published to MQTT when a live market tick is received.
|
||||
/// </summary>
|
||||
public record TickMessageDto(
|
||||
[property: JsonPropertyName("price")] decimal Price
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for fetching trade proposals from FinlyticEngine.
|
||||
/// </summary>
|
||||
public record GetTradeProposalsRequest(
|
||||
[property: JsonPropertyName("onlyActive")] bool OnlyActive = true,
|
||||
[property: JsonPropertyName("limit")] int Limit = 50
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for fetching active trades from FinlyticEngine. <see cref="UserId"/> is mandatory
|
||||
/// (not defaulted/optional) so FinlyticEngine always filters trades to their owner server-side; a caller
|
||||
/// can never accidentally list every user's trades by omitting it (see Rules.md multi-tenancy requirement).
|
||||
/// </summary>
|
||||
public record GetActiveTradesRequest(
|
||||
[property: JsonPropertyName("userId")] Guid UserId,
|
||||
[property: JsonPropertyName("mode")] FinlyticCore.Dtos.Trading.ExecutionMode? Mode = null
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for triggering an on-demand evaluation in FinlyticEngine. <see cref="UserId"/> identifies
|
||||
/// the human caller for the resulting <c>EngineEvaluationSnapshotEntity.TriggeredByUserId</c> audit trail
|
||||
/// (this RPC channel is only ever reached from the manual Web UI flows - the autonomous
|
||||
/// <c>OpportunityPollerBackgroundService</c> calls <c>ITradeLifecycleService.EvaluateAssetAsync</c> directly
|
||||
/// in-process and never goes through this channel at all). Exactly like <see cref="AddTradeFillRequest.UserId"/>
|
||||
/// and its siblings, any value supplied by an untrusted client is discarded and overwritten server-side
|
||||
/// (FinlyticBackend) with the identity from the JWT before the request is forwarded over MQTT; the default of
|
||||
/// <see cref="Guid.Empty"/> here only exists so <see cref="Ticker"/>/<see cref="ForceAiEvaluation"/> can keep
|
||||
/// their own defaults (C# requires optional parameters to trail).
|
||||
/// </summary>
|
||||
public record EvaluateAssetRequest(
|
||||
[property: JsonPropertyName("isin")] string Isin,
|
||||
[property: JsonPropertyName("userId")] Guid UserId = default,
|
||||
[property: JsonPropertyName("ticker")] string? Ticker = null,
|
||||
[property: JsonPropertyName("forceAiEvaluation")] bool ForceAiEvaluation = false
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for adding an executed fill to an active trade. <see cref="UserId"/> is mandatory so
|
||||
/// FinlyticEngine can verify the caller owns <see cref="TradeId"/> before mutating it; a value supplied by an
|
||||
/// untrusted client must always be overwritten server-side (FinlyticBackend) with the identity from the JWT.
|
||||
/// </summary>
|
||||
public record AddTradeFillRequest(
|
||||
[property: JsonPropertyName("userId")] Guid UserId,
|
||||
[property: JsonPropertyName("tradeId")] Guid TradeId,
|
||||
[property: JsonPropertyName("executedPrice")] decimal ExecutedPrice,
|
||||
[property: JsonPropertyName("quantity")] decimal Quantity,
|
||||
[property: JsonPropertyName("fee")] decimal Fee = 0m,
|
||||
[property: JsonPropertyName("note")] string? Note = null
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for manually or algorithmically adjusting a trade's stop loss. <see cref="UserId"/> is
|
||||
/// mandatory so FinlyticEngine can verify the caller owns <see cref="TradeId"/> before mutating it; a value
|
||||
/// supplied by an untrusted client must always be overwritten server-side (FinlyticBackend) with the identity
|
||||
/// from the JWT.
|
||||
/// </summary>
|
||||
public record UpdateTradeStopLossRequest(
|
||||
[property: JsonPropertyName("userId")] Guid UserId,
|
||||
[property: JsonPropertyName("tradeId")] Guid TradeId,
|
||||
[property: JsonPropertyName("newStopLoss")] decimal NewStopLoss,
|
||||
[property: JsonPropertyName("reason")] string Reason
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for closing an active trade. <see cref="UserId"/> is mandatory so FinlyticEngine can verify
|
||||
/// the caller owns <see cref="TradeId"/> before closing it; a value supplied by an untrusted client must always
|
||||
/// be overwritten server-side (FinlyticBackend) with the identity from the JWT.
|
||||
/// </summary>
|
||||
public record CloseEngineTradeRequest(
|
||||
[property: JsonPropertyName("userId")] Guid UserId,
|
||||
[property: JsonPropertyName("tradeId")] Guid TradeId,
|
||||
[property: JsonPropertyName("closePrice")] decimal ClosePrice,
|
||||
[property: JsonPropertyName("reason")] string Reason
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for accepting an open trade proposal on behalf of a single user. A proposal is a
|
||||
/// system-wide opportunity, so accepting it does NOT consume or deactivate it — it creates one independent
|
||||
/// trade owned by <see cref="UserId"/>, and other users may still accept the same proposal. Proposals
|
||||
/// disappear on their own once <c>ExpiresAtUtc</c> passes; there is deliberately no "reject" round trip,
|
||||
/// because declining a proposal has no server-side effect.
|
||||
/// <see cref="UserId"/> must always be overwritten server-side (FinlyticBackend) with the identity from
|
||||
/// the JWT and never trusted from the client.
|
||||
/// </summary>
|
||||
public record AcceptTradeProposalRequest(
|
||||
[property: JsonPropertyName("userId")] Guid UserId,
|
||||
[property: JsonPropertyName("proposalId")] Guid ProposalId,
|
||||
[property: JsonPropertyName("executedPrice")] decimal? ExecutedPrice = null,
|
||||
[property: JsonPropertyName("quantity")] decimal? Quantity = null
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Request payload for manually opening a trade in FinlyticEngine with no backing proposal (e.g. a user
|
||||
/// enters a position in the Web UI that FinlyticEngine never evaluated or scored). <see cref="UserId"/> is
|
||||
/// mandatory and must always be overwritten server-side (FinlyticBackend) with the identity from the JWT,
|
||||
/// exactly like every other engine trade-mutation request.
|
||||
/// There is deliberately no <c>ProposalId</c> field: <c>EngineTradeEntity.ProposalId</c> stays a
|
||||
/// non-nullable <see cref="Guid"/> everywhere else in the codebase (grouping trades that share one accepted
|
||||
/// proposal), so FinlyticEngine substitutes <see cref="Guid.Empty"/> for a manually created trade instead of
|
||||
/// widening that column to nullable for the sake of this single caller.
|
||||
/// </summary>
|
||||
public record CreateManualTradeRequest(
|
||||
[property: JsonPropertyName("userId")] Guid UserId,
|
||||
[property: JsonPropertyName("underlyingIsin")] string UnderlyingIsin,
|
||||
[property: JsonPropertyName("symbol")] string Symbol,
|
||||
[property: JsonPropertyName("direction")] FinlyticCore.Dtos.TechnicalAnalysis.SignalDirection Direction,
|
||||
[property: JsonPropertyName("entryPrice")] decimal EntryPrice,
|
||||
[property: JsonPropertyName("quantity")] decimal Quantity,
|
||||
[property: JsonPropertyName("initialStopLoss")] decimal InitialStopLoss,
|
||||
[property: JsonPropertyName("takeProfit1")] decimal TakeProfit1,
|
||||
[property: JsonPropertyName("takeProfit2")] decimal? TakeProfit2 = null,
|
||||
[property: JsonPropertyName("instrumentType")] FinlyticCore.Dtos.Trading.InstrumentCategoryType InstrumentType = FinlyticCore.Dtos.Trading.InstrumentCategoryType.Stock,
|
||||
[property: JsonPropertyName("derivativeIsin")] string? DerivativeIsin = null,
|
||||
[property: JsonPropertyName("derivativeWkn")] string? DerivativeWkn = null,
|
||||
[property: JsonPropertyName("fee")] decimal Fee = 0m
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Machine-readable classification of a server-side RPC fault, carried by <see cref="RpcErrorResponse"/> so a
|
||||
/// caller can react to the specific failure mode instead of only learning "something went wrong" (or, before
|
||||
/// this error channel existed, learning nothing at all and simply timing out). The set is deliberately small and
|
||||
/// mirrors the handful of exception shapes actually thrown by <c>SubscribeRpcAsync</c> handlers across the
|
||||
/// fleet today (see <see cref="FinlyticCore.Util.ManagedMqttClient"/>); it is not meant to be a full HTTP-status
|
||||
/// mirror. Each value has a corresponding standard .NET exception type that
|
||||
/// <see cref="FinlyticCore.Util.ManagedMqttClient"/> reconstructs client-side, so existing
|
||||
/// <c>catch (InvalidOperationException)</c> / <c>catch (ArgumentException)</c> blocks written against the
|
||||
/// service-layer methods' local exception types keep working unchanged across the MQTT boundary.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<RpcFaultCode>))]
|
||||
public enum RpcFaultCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Uncategorized/unexpected server-side failure with no safe, specific detail to disclose over MQTT (the
|
||||
/// broker runs without authentication). The full exception is logged locally on the serving side only.
|
||||
/// </summary>
|
||||
Internal = 0,
|
||||
|
||||
/// <summary>The request conflicts with current server-side state (e.g. a proposal already accepted by this same user).</summary>
|
||||
Conflict = 1,
|
||||
|
||||
/// <summary>The request payload failed validation (e.g. a blank ISIN or a non-positive price/quantity).</summary>
|
||||
InvalidArgument = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The referenced resource does not exist, or exists but does not belong to the caller. The two cases are
|
||||
/// deliberately not distinguished (see the multi-tenancy note on <see cref="GetActiveTradesRequest"/>): a
|
||||
/// caller must never learn that a trade ID exists under another user's account.
|
||||
/// </summary>
|
||||
NotFound = 3,
|
||||
|
||||
/// <summary>The caller's identity could not be established, or is not permitted to perform this operation.</summary>
|
||||
Unauthorized = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Typed error envelope published by <see cref="FinlyticCore.Util.ManagedMqttClient.SubscribeRpcAsync{TRequest,TResponse}"/>
|
||||
/// on a dedicated error sub-topic when an RPC handler throws, instead of silently dropping the request and
|
||||
/// leaving the caller to hit its request timeout. The message carries only a machine-readable
|
||||
/// <see cref="Code"/> and a short, safe, fully-formed <see cref="Message"/>; internal details (stack traces,
|
||||
/// connection strings, etc.) are never placed on the wire and must be logged locally on the serving side instead
|
||||
/// (Rules.md §10/§11, and the MQTT broker currently has no authentication).
|
||||
/// </summary>
|
||||
public record RpcErrorResponse(
|
||||
[property: JsonPropertyName("code")] RpcFaultCode Code,
|
||||
[property: JsonPropertyName("message")] string Message
|
||||
);
|
||||
@@ -40,7 +40,7 @@ public record FinBertResultDto
|
||||
/// <summary>
|
||||
/// Gets or sets the compound score (-1.0 to +1.0).
|
||||
/// </summary>
|
||||
[JsonPropertyName("compoundScore")]
|
||||
[JsonPropertyName("compound_score")]
|
||||
public double CompoundScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
@@ -49,6 +49,12 @@ public record FinBertResultDto
|
||||
[JsonPropertyName("confidence")]
|
||||
public double Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the estimated market impact ("HIGH", "MEDIUM", "LOW").
|
||||
/// </summary>
|
||||
[JsonPropertyName("impact")]
|
||||
public string? Impact { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the probability breakdown.
|
||||
/// </summary>
|
||||
@@ -56,8 +62,14 @@ public record FinBertResultDto
|
||||
public FinBertProbabilities Probabilities { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the short summary snippet highlighting the impact of the article.
|
||||
/// Gets or sets the short key highlight extracted by FinBERT / n8n.
|
||||
/// </summary>
|
||||
[JsonPropertyName("key_highlight")]
|
||||
public string? KeyHighlight { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Legacy alias for KeyHighlight / summary snippet.
|
||||
/// </summary>
|
||||
[JsonPropertyName("summarySnippet")]
|
||||
public string? SummarySnippet { get; init; }
|
||||
public string? SummarySnippet => KeyHighlight;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace FinlyticCore.Dtos.Sentiment;
|
||||
@@ -97,6 +98,36 @@ public record IsinCurrentSummary
|
||||
[JsonPropertyName("totalArticlesAnalyzed")]
|
||||
public int TotalArticlesAnalyzed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of positive articles.
|
||||
/// </summary>
|
||||
[JsonPropertyName("positiveArticles")]
|
||||
public int PositiveArticles { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of negative articles.
|
||||
/// </summary>
|
||||
[JsonPropertyName("negativeArticles")]
|
||||
public int NegativeArticles { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of neutral articles.
|
||||
/// </summary>
|
||||
[JsonPropertyName("neutralArticles")]
|
||||
public int NeutralArticles { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sentiment trend ("IMPROVING", "DETERIORATING", "STABLE").
|
||||
/// </summary>
|
||||
[JsonPropertyName("trend")]
|
||||
public string? Trend { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the key highlight summary.
|
||||
/// </summary>
|
||||
[JsonPropertyName("keyHighlight")]
|
||||
public string? KeyHighlight { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the overall synthesized sentiment text overview.
|
||||
/// </summary>
|
||||
@@ -105,7 +136,7 @@ public record IsinCurrentSummary
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data transfer object for an ISIN sentiment summary file (stored in data/summaries/isin/ISIN.json).
|
||||
/// Data transfer object for an ISIN sentiment summary file.
|
||||
/// </summary>
|
||||
public record IsinSentimentSummaryDto
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace FinlyticCore.Dtos.Sentiment;
|
||||
@@ -55,6 +56,18 @@ public record SectorCurrentSummary
|
||||
[JsonPropertyName("sentimentLabel")]
|
||||
public string SentimentLabel { get; init; } = "NEUTRAL";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of articles analyzed for this sector.
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalArticlesAnalyzed")]
|
||||
public int TotalArticlesAnalyzed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total number of distinct companies in this sector.
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalCompanies")]
|
||||
public int TotalCompanies { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of active asset ISINs influencing the sector.
|
||||
/// </summary>
|
||||
@@ -69,7 +82,7 @@ public record SectorCurrentSummary
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data transfer object for a Sector sentiment summary file (stored in data/summaries/sectors/SectorName.json).
|
||||
/// Data transfer object for a Sector sentiment summary file.
|
||||
/// </summary>
|
||||
public record SectorSentimentSummaryDto
|
||||
{
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||
|
||||
namespace FinlyticCore.Dtos.Simulation;
|
||||
|
||||
/// <param name="StrategyParameters">
|
||||
/// Per-run overrides for <paramref name="StrategyKey"/>'s tunable indicator parameters, keyed by
|
||||
/// <c>"{StrategyKey}.{ParameterName}"</c> (e.g. <c>"MeanReversion.RsiOversold"</c>) - see
|
||||
/// <c>TechnicalContext.ParameterOverrides</c>. <see langword="null"/>/empty means "use that strategy's own
|
||||
/// hardcoded defaults". Deliberately scoped to backtesting only - live scanning never applies these.
|
||||
/// </param>
|
||||
public record BacktestRequestDto(
|
||||
string Isin,
|
||||
string Symbol,
|
||||
string StrategyKey,
|
||||
string Timeframe,
|
||||
DateTime StartDateUtc,
|
||||
DateTime EndDateUtc,
|
||||
decimal StartingCapital = 10000m,
|
||||
decimal RiskPerTradePercent = 1.0m, // 1% Risiko pro Trade
|
||||
bool IncludeFeesAndSlippage = true,
|
||||
bool SimulateKnockOutDerivatives = false,
|
||||
decimal? TargetLeverage = 5.0m,
|
||||
Dictionary<string, decimal>? StrategyParameters = null
|
||||
);
|
||||
|
||||
public record BacktestTradeDto(
|
||||
Guid TradeId,
|
||||
DateTime EntryTimeUtc,
|
||||
DateTime ExitTimeUtc,
|
||||
SignalDirection Direction,
|
||||
decimal EntryPrice,
|
||||
decimal ExitPrice,
|
||||
decimal Quantity,
|
||||
decimal InitialStopLoss,
|
||||
decimal RealizedPnlEur,
|
||||
decimal ReturnPercent,
|
||||
decimal RMultiple,
|
||||
string ExitReason, // "TP1_Hit", "TP2_Hit", "BreakEven", "TrailingStop", "KnockedOut", "TimeExpired"
|
||||
decimal MaxAdverseExcursionPercent, // MAE: Maximaler zwischenzeitlicher Buchverlust
|
||||
decimal MaxFavorableExcursionPercent // MFE: Maximaler zwischenzeitlicher Buchgewinn
|
||||
);
|
||||
|
||||
public record EquityPointDto(
|
||||
DateTime TimestampUtc,
|
||||
decimal PortfolioValue,
|
||||
decimal DrawdownPercent
|
||||
);
|
||||
|
||||
public record BacktestReportDto(
|
||||
Guid RunId,
|
||||
string Isin,
|
||||
string Symbol,
|
||||
string StrategyKey,
|
||||
string Timeframe,
|
||||
DateTime StartDateUtc,
|
||||
DateTime EndDateUtc,
|
||||
int TotalTrades,
|
||||
int WinningTrades,
|
||||
int LosingTrades,
|
||||
decimal WinRatePercent,
|
||||
decimal ProfitFactor,
|
||||
decimal MaxDrawdownPercent,
|
||||
decimal TotalReturnPercent,
|
||||
decimal ExpectancyEur,
|
||||
decimal SharpeRatio,
|
||||
decimal AverageRiskRewardRatio,
|
||||
TimeSpan AverageHoldingDuration,
|
||||
List<BacktestTradeDto> Trades,
|
||||
List<EquityPointDto> EquityCurve
|
||||
);
|
||||
|
||||
public record StrategyAssetReliabilityDto(
|
||||
string Isin,
|
||||
string StrategyKey,
|
||||
decimal ReliabilityScore, // 0 - 100
|
||||
decimal WinRatePercent,
|
||||
decimal ProfitFactor,
|
||||
int SampleTradeCount,
|
||||
bool IsStrategyApprovedForAsset,
|
||||
string RecommendedAction // "BOOST_SCORE", "NEUTRAL", "VETO_DISABLE"
|
||||
);
|
||||
|
||||
public record GetReliabilityRequest(
|
||||
string Isin,
|
||||
string StrategyKey,
|
||||
string Timeframe = "15m"
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Filters for <c>MqttTopics.Channels.SimGetBacktestHistory</c>. <see cref="StrategyKey"/> is optional -
|
||||
/// <see langword="null"/> returns every strategy's runs for the ISIN, so the Web UI can show "all history for
|
||||
/// this asset" and let the user narrow down from there.
|
||||
/// </summary>
|
||||
public record GetBacktestHistoryRequest(
|
||||
string Isin,
|
||||
string? StrategyKey = null,
|
||||
int Limit = 20
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// One row of the backtest history list - a lightweight summary (no <c>Trades</c>/<c>EquityCurve</c>) mapped
|
||||
/// 1:1 from a persisted <c>SimulationRunEntity</c>, so listing many runs for an asset stays cheap. Fetch the
|
||||
/// full <see cref="BacktestReportDto"/> for one specific run via <c>SimGetBacktestRunDetail</c> when the user
|
||||
/// drills into it.
|
||||
/// </summary>
|
||||
public record BacktestHistoryEntryDto(
|
||||
Guid RunId,
|
||||
string Isin,
|
||||
string Symbol,
|
||||
string StrategyKey,
|
||||
string Timeframe,
|
||||
DateTime StartDateUtc,
|
||||
DateTime EndDateUtc,
|
||||
int TotalTrades,
|
||||
decimal WinRatePercent,
|
||||
decimal ProfitFactor,
|
||||
decimal MaxDrawdownPercent,
|
||||
decimal TotalReturnPercent,
|
||||
decimal SharpeRatio,
|
||||
DateTime CreatedAtUtc
|
||||
);
|
||||
|
||||
/// <summary>Looks up one specific past backtest run's full report by its RunId (<c>MqttTopics.Channels.SimGetBacktestRunDetail</c>).</summary>
|
||||
public record GetBacktestRunDetailRequest(Guid RunId);
|
||||
|
||||
/// <summary>Looks up a saved parameter profile for one (Isin, StrategyKey) pair (<c>MqttTopics.Channels.SimGetStrategyParameters</c>).</summary>
|
||||
public record GetStrategyParametersRequest(string Isin, string StrategyKey);
|
||||
|
||||
/// <summary>Upserts a saved parameter profile for one (Isin, StrategyKey) pair (<c>MqttTopics.Channels.SimSaveStrategyParameters</c>).</summary>
|
||||
public record SaveStrategyParametersRequest(string Isin, string StrategyKey, Dictionary<string, decimal> Parameters);
|
||||
|
||||
/// <summary>
|
||||
/// A saved set of tunable indicator parameter overrides for one (Isin, StrategyKey) pair, keyed by
|
||||
/// <c>"{StrategyKey}.{ParameterName}"</c> (matching <c>TechnicalContext.ParameterOverrides</c> 1:1) - see
|
||||
/// <c>SimulationStrategyParameterEntity</c>.
|
||||
/// </summary>
|
||||
public record StrategyParameterProfileDto(
|
||||
string Isin,
|
||||
string StrategyKey,
|
||||
Dictionary<string, decimal> Parameters,
|
||||
DateTime UpdatedAtUtc
|
||||
);
|
||||
@@ -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);
|
||||
@@ -1,17 +1,18 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace FinlyticCore.Dtos.TradeRepublic;
|
||||
|
||||
public record TradeRepublicPriceTick(
|
||||
[property: JsonPropertyName("time")] long Time,
|
||||
[property: JsonPropertyName("price")] string Price,
|
||||
[property: JsonPropertyName("size")] decimal Size
|
||||
[property: JsonPropertyName("time")] long? Time = null,
|
||||
[property: JsonPropertyName("price"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] decimal Price = 0m,
|
||||
[property: JsonPropertyName("size")] decimal? Size = null
|
||||
)
|
||||
{
|
||||
public decimal PriceValue => decimal.TryParse(Price, NumberStyles.Any, CultureInfo.InvariantCulture, out var v) ? v : 0m;
|
||||
public DateTime DateTimeUtc => DateTimeOffset.FromUnixTimeMilliseconds(Time).UtcDateTime;
|
||||
public decimal PriceValue => Price;
|
||||
public DateTime DateTimeUtc => Time.HasValue && Time.Value > 0
|
||||
? DateTimeOffset.FromUnixTimeMilliseconds(Time.Value).UtcDateTime
|
||||
: DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public record TradeRepublicTickerResponse(
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||
|
||||
namespace FinlyticCore.Dtos.Trading;
|
||||
|
||||
public record DerivativeSelectionDto(
|
||||
[property: JsonPropertyName("derivativeIsin")] string DerivativeIsin,
|
||||
// Trade Republic liefert für Derivate keine WKN, nur die ISIN (siehe TradeRepublicDerivativeItemDto).
|
||||
// Daher ist dieses Feld nullable: eine ISIN darf hier NICHT als Ersatz-WKN eingetragen werden (Rules.md §4).
|
||||
[property: JsonPropertyName("derivativeWkn")] string? DerivativeWkn,
|
||||
[property: JsonPropertyName("issuer")] string Issuer,
|
||||
[property: JsonPropertyName("optionType")] string OptionType, // "LONG" oder "SHORT"
|
||||
[property: JsonPropertyName("strike")] decimal Strike,
|
||||
[property: JsonPropertyName("barrier")] decimal Barrier,
|
||||
[property: JsonPropertyName("leverage")] decimal Leverage,
|
||||
[property: JsonPropertyName("safetyBufferPercent")] decimal SafetyBufferPercent,
|
||||
[property: JsonPropertyName("spreadPercentage")] decimal SpreadPercentage,
|
||||
[property: JsonPropertyName("size")] decimal Size
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Kennzeichnet die Herkunft einer <see cref="AiValidationResultDto"/>-Entscheidung, damit
|
||||
/// Konsumenten (Frontend, Logs) eine echte KI-Analyse von einer regelbasierten Ersatzentscheidung
|
||||
/// unterscheiden können. Der Enum-Wert <see cref="Ai"/> ist absichtlich der Default (0), damit ein
|
||||
/// vom N8N-Webhook geliefertes JSON, das dieses Feld (noch) nicht setzt, korrekt als KI-Ergebnis
|
||||
/// interpretiert wird.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<ValidationSource>))]
|
||||
public enum ValidationSource
|
||||
{
|
||||
Ai,
|
||||
RuleBased
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ergebnis des AI-Reasoning-Gates. <see cref="Confidence"/> ist nur gesetzt, wenn <see cref="Source"/>
|
||||
/// den Wert <see cref="ValidationSource.Ai"/> hat, da eine Konfidenz ohne tatsächliche KI-Bewertung
|
||||
/// erfunden wäre (Rules.md §4).
|
||||
/// </summary>
|
||||
public record AiValidationResultDto(
|
||||
[property: JsonPropertyName("isApproved")] bool IsApproved,
|
||||
[property: JsonPropertyName("confidence")] decimal? Confidence,
|
||||
[property: JsonPropertyName("validationSource")] ValidationSource Source,
|
||||
[property: JsonPropertyName("thesisSummary")] string ThesisSummary,
|
||||
[property: JsonPropertyName("invalidationReason")] string InvalidationReason,
|
||||
[property: JsonPropertyName("keyCatalysts")] List<string> KeyCatalysts,
|
||||
[property: JsonPropertyName("identifiedRisks")] List<string> IdentifiedRisks
|
||||
);
|
||||
|
||||
public record TradeProposalDto(
|
||||
[property: JsonPropertyName("proposalId")] Guid ProposalId,
|
||||
[property: JsonPropertyName("underlyingIsin")] string UnderlyingIsin,
|
||||
[property: JsonPropertyName("symbol")] string Symbol,
|
||||
[property: JsonPropertyName("strategyKey")] string StrategyKey,
|
||||
[property: JsonPropertyName("direction")] SignalDirection Direction,
|
||||
[property: JsonPropertyName("qualityScore")] decimal QualityScore,
|
||||
[property: JsonPropertyName("compositeScore")] decimal CompositeScore,
|
||||
[property: JsonPropertyName("currentPrice")] decimal CurrentPrice,
|
||||
[property: JsonPropertyName("entryPrice")] decimal EntryPrice,
|
||||
[property: JsonPropertyName("invalidationPrice")] decimal InvalidationPrice,
|
||||
[property: JsonPropertyName("exitPlan")] ExitPlan ExitPlan,
|
||||
[property: JsonPropertyName("selectedDerivative")] DerivativeSelectionDto? SelectedDerivative,
|
||||
[property: JsonPropertyName("aiValidation")] AiValidationResultDto AiValidation,
|
||||
[property: JsonPropertyName("createdAtUtc")] DateTime CreatedAtUtc,
|
||||
[property: JsonPropertyName("expiresAtUtc")] DateTime ExpiresAtUtc
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Full result of <c>ITradeLifecycleService.EvaluateAssetAsync</c>, carrying both possible outcomes of the
|
||||
/// evaluation pipeline (technicals, sentiment, fundamentals, simulation-reliability, AI reasoning gate):
|
||||
/// an accepted opportunity (<see cref="Proposal"/> is set) or a rejection, in which case <see cref="Proposal"/>
|
||||
/// is <see langword="null"/> but every score/reasoning field below is still populated with the real,
|
||||
/// already-computed values instead of leaving the caller with silence (Rules.md §4).
|
||||
/// <para>
|
||||
/// When the pipeline could not even produce a score (no technical setups available for the ISIN, or the
|
||||
/// ISIN was blank), the score fields are <c>0</c> and <see cref="AiThesisSummary"/> carries a
|
||||
/// "<c>[Regelbasiert]</c>"-prefixed explanation — the same prefix <see cref="AiValidationResultDto"/> uses for
|
||||
/// its <see cref="ValidationSource.RuleBased"/> fallback — so a caller/UI can recognize this is not a real
|
||||
/// AI verdict, just as it already must for a rule-based <see cref="AiValidationResultDto"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public record AssetEvaluationResultDto(
|
||||
[property: JsonPropertyName("proposal")] TradeProposalDto? Proposal,
|
||||
[property: JsonPropertyName("compositeScore")] decimal CompositeScore,
|
||||
[property: JsonPropertyName("technicalScore")] decimal TechnicalScore,
|
||||
[property: JsonPropertyName("sentimentScore")] decimal SentimentScore,
|
||||
[property: JsonPropertyName("fundamentalScore")] decimal FundamentalScore,
|
||||
[property: JsonPropertyName("passedEarningsLockout")] bool PassedEarningsLockout,
|
||||
[property: JsonPropertyName("daysToNextEarnings")] int? DaysToNextEarnings,
|
||||
[property: JsonPropertyName("passedDividendGate")] bool PassedDividendGate,
|
||||
[property: JsonPropertyName("daysToNextExDividend")] int? DaysToNextExDividend,
|
||||
[property: JsonPropertyName("aiApproved")] bool AiApproved,
|
||||
[property: JsonPropertyName("aiThesisSummary")] string AiThesisSummary,
|
||||
[property: JsonPropertyName("aiIdentifiedRisks")] List<string> AiIdentifiedRisks
|
||||
);
|
||||
|
||||
public record TradeFillDto(
|
||||
[property: JsonPropertyName("fillId")] Guid FillId,
|
||||
[property: JsonPropertyName("executedAtUtc")] DateTime ExecutedAtUtc,
|
||||
[property: JsonPropertyName("price")] decimal Price,
|
||||
[property: JsonPropertyName("quantity")] decimal Quantity,
|
||||
[property: JsonPropertyName("fee")] decimal Fee,
|
||||
[property: JsonPropertyName("note")] string? Note
|
||||
);
|
||||
|
||||
public record ActiveTradeDto(
|
||||
[property: JsonPropertyName("tradeId")] Guid TradeId,
|
||||
[property: JsonPropertyName("proposalId")] Guid ProposalId,
|
||||
[property: JsonPropertyName("underlyingIsin")] string UnderlyingIsin,
|
||||
[property: JsonPropertyName("symbol")] string Symbol,
|
||||
[property: JsonPropertyName("derivativeIsin")] string? DerivativeIsin,
|
||||
[property: JsonPropertyName("derivativeWkn")] string? DerivativeWkn,
|
||||
[property: JsonPropertyName("executionMode")] ExecutionMode ExecutionMode,
|
||||
[property: JsonPropertyName("instrumentType")] InstrumentCategoryType InstrumentType,
|
||||
[property: JsonPropertyName("direction")] SignalDirection Direction,
|
||||
[property: JsonPropertyName("status")] TradeStatus Status,
|
||||
[property: JsonPropertyName("averageBuyIn")] decimal AverageBuyIn,
|
||||
[property: JsonPropertyName("totalQuantity")] decimal TotalQuantity,
|
||||
[property: JsonPropertyName("initialStopLoss")] decimal InitialStopLoss,
|
||||
[property: JsonPropertyName("currentStopLoss")] decimal CurrentStopLoss,
|
||||
[property: JsonPropertyName("currentPrice")] decimal CurrentPrice,
|
||||
[property: JsonPropertyName("unrealizedPnlEur")] decimal UnrealizedPnlEur,
|
||||
[property: JsonPropertyName("unrealizedPnlPercent")] decimal UnrealizedPnlPercent,
|
||||
[property: JsonPropertyName("realizedPnlEur")] decimal RealizedPnlEur,
|
||||
[property: JsonPropertyName("exitPlan")] ExitPlan ExitPlan,
|
||||
[property: JsonPropertyName("fills")] List<TradeFillDto> Fills,
|
||||
[property: JsonPropertyName("openedAtUtc")] DateTime OpenedAtUtc,
|
||||
[property: JsonPropertyName("closedAtUtc")] DateTime? ClosedAtUtc
|
||||
);
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||
|
||||
namespace FinlyticCore.Dtos.Trading;
|
||||
|
||||
/// <summary>
|
||||
/// Filter/pagination request for the admin-only evaluation-history RPC channel
|
||||
/// (<c>MqttTopics.Channels.EngineGetEvaluationHistory</c>), served by FinlyticEngine and exposed to the Web UI
|
||||
/// via <c>FinlyticBackend/Controllers/AdminEvaluationHistoryController</c>. All filters are optional and are
|
||||
/// combined with logical AND; <see langword="null"/> means "do not filter on this field".
|
||||
/// </summary>
|
||||
/// <param name="FromUtc">Inclusive lower bound on <c>EngineEvaluationSnapshotEntity.EvaluatedAtUtc</c>.</param>
|
||||
/// <param name="ToUtc">Inclusive upper bound on <c>EngineEvaluationSnapshotEntity.EvaluatedAtUtc</c>.</param>
|
||||
/// <param name="OutcomeFilter">Restricts results to a single <see cref="OutcomeReason"/>.</param>
|
||||
/// <param name="TriggerSourceFilter">Restricts results to a single <see cref="TriggerSource"/>.</param>
|
||||
/// <param name="IsinOrSymbolSearch">
|
||||
/// Case-sensitive substring search against both <c>Isin</c> and <c>Symbol</c> (matches either). Trimmed
|
||||
/// server-side; blank/whitespace-only values are treated as "no search".
|
||||
/// </param>
|
||||
/// <param name="Page">1-based page number. Values below 1 are treated as 1 server-side.</param>
|
||||
/// <param name="PageSize">
|
||||
/// Requested page size. Server-side clamped to at least 1 and at most 200 (see
|
||||
/// <c>EvaluationHistoryService.MaxPageSize</c>) so a caller cannot force FinlyticEngine to materialize/transmit
|
||||
/// an unbounded result set in a single response.
|
||||
/// </param>
|
||||
public record GetEvaluationHistoryRequest(
|
||||
[property: JsonPropertyName("fromUtc")] System.DateTime? FromUtc = null,
|
||||
[property: JsonPropertyName("toUtc")] System.DateTime? ToUtc = null,
|
||||
[property: JsonPropertyName("outcomeFilter")] OutcomeReason? OutcomeFilter = null,
|
||||
[property: JsonPropertyName("triggerSourceFilter")] TriggerSource? TriggerSourceFilter = null,
|
||||
[property: JsonPropertyName("isinOrSymbolSearch")] string? IsinOrSymbolSearch = null,
|
||||
[property: JsonPropertyName("page")] int Page = 1,
|
||||
[property: JsonPropertyName("pageSize")] int PageSize = 50
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// One row of the evaluation history: the full, already-persisted record of a single
|
||||
/// <c>TradeLifecycleService.EvaluateAssetAsync</c> run, mapped 1:1 from <c>EngineEvaluationSnapshotEntity</c>.
|
||||
/// Every score field is the real, already-computed value - including the honest "0/default" values recorded
|
||||
/// for the <see cref="OutcomeReason.NoTechnicalSetups"/> case, never a fabricated placeholder (Rules.md §4).
|
||||
/// </summary>
|
||||
public record EvaluationHistoryEntryDto(
|
||||
[property: JsonPropertyName("id")] System.Guid Id,
|
||||
[property: JsonPropertyName("isin")] string Isin,
|
||||
[property: JsonPropertyName("symbol")] string Symbol,
|
||||
[property: JsonPropertyName("technicalScore")] decimal TechnicalScore,
|
||||
[property: JsonPropertyName("sentimentScore")] decimal SentimentScore,
|
||||
[property: JsonPropertyName("fundamentalScore")] decimal FundamentalScore,
|
||||
[property: JsonPropertyName("compositeOpportunityScore")] decimal CompositeOpportunityScore,
|
||||
[property: JsonPropertyName("reliabilityBonus")] decimal ReliabilityBonus,
|
||||
[property: JsonPropertyName("passedEarningsLockout")] bool PassedEarningsLockout,
|
||||
[property: JsonPropertyName("daysToNextEarnings")] int? DaysToNextEarnings,
|
||||
[property: JsonPropertyName("passedDividendGate")] bool PassedDividendGate,
|
||||
[property: JsonPropertyName("daysToNextExDividend")] int? DaysToNextExDividend,
|
||||
[property: JsonPropertyName("universeSource")] UniverseSource? UniverseSource,
|
||||
[property: JsonPropertyName("universeEnteredAtUtc")] System.DateTime? UniverseEnteredAtUtc,
|
||||
[property: JsonPropertyName("passedSimulationVeto")] bool PassedSimulationVeto,
|
||||
[property: JsonPropertyName("passedAiValidation")] bool PassedAiValidation,
|
||||
[property: JsonPropertyName("aiThesisSummary")] string AiThesisSummary,
|
||||
[property: JsonPropertyName("outcomeReason")] OutcomeReason OutcomeReason,
|
||||
[property: JsonPropertyName("triggerSource")] TriggerSource TriggerSource,
|
||||
[property: JsonPropertyName("triggeredByUserId")] System.Guid? TriggeredByUserId,
|
||||
[property: JsonPropertyName("proposalId")] System.Guid? ProposalId,
|
||||
[property: JsonPropertyName("evaluatedAtUtc")] System.DateTime EvaluatedAtUtc
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Number of evaluation-history rows matching a given filter set that carry a specific <see cref="OutcomeReason"/>.
|
||||
/// A typed list of these (rather than a <c>Dictionary<OutcomeReason,int></c>) is used on
|
||||
/// <see cref="EvaluationHistorySummaryDto.CountsByOutcome"/> purely so this DTO round-trips through
|
||||
/// System.Text.Json (including the AOT source-generated <c>FinlyticJsonSerializerContext</c>) without needing a
|
||||
/// custom enum-keyed dictionary converter.
|
||||
/// </summary>
|
||||
public record OutcomeReasonCountDto(
|
||||
[property: JsonPropertyName("outcomeReason")] OutcomeReason OutcomeReason,
|
||||
[property: JsonPropertyName("count")] int Count
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-aggregated headline numbers for the admin evaluation-history tab (e.g. "1.847 Analysen letzte 24h ·
|
||||
/// 0 Vorschläge seit 14h · Ø-Score 66,7"), computed server-side so the Web UI never has to aggregate the full,
|
||||
/// unpaginated result set itself. Every field except <see cref="LastProposalCreatedAtUtc"/> is scoped to
|
||||
/// exactly the same filters as the paginated <see cref="EvaluationHistoryEntryDto"/> list it accompanies (see
|
||||
/// <see cref="GetEvaluationHistoryResponse"/>) - only pagination (<c>Page</c>/<c>PageSize</c>) does not apply,
|
||||
/// since these are totals over the whole filtered set, not just the current page.
|
||||
/// </summary>
|
||||
/// <param name="TotalEvaluations">Total number of snapshot rows matching the request's filters (unpaginated).</param>
|
||||
/// <param name="CountsByOutcome">Breakdown of <see cref="TotalEvaluations"/> by <see cref="OutcomeReason"/>.</param>
|
||||
/// <param name="AverageCompositeScore">
|
||||
/// Average <c>CompositeOpportunityScore</c> across the filtered set; <c>0</c> when <see cref="TotalEvaluations"/> is 0.
|
||||
/// </param>
|
||||
/// <param name="ProposalsCreated">
|
||||
/// Number of filtered rows whose <see cref="EvaluationHistoryEntryDto.OutcomeReason"/> is
|
||||
/// <see cref="OutcomeReason.Approved"/> - i.e. the same value as the <see cref="OutcomeReason.Approved"/> entry
|
||||
/// in <see cref="CountsByOutcome"/>, exposed directly so the UI does not need to search that list.
|
||||
/// </param>
|
||||
/// <param name="LastProposalCreatedAtUtc">
|
||||
/// Timestamp of the most recently created <c>EngineTradeProposalEntity</c> across the ENTIRE proposals table -
|
||||
/// deliberately NOT scoped to this request's <c>FromUtc</c>/<c>ToUtc</c> filters, because "how long since the
|
||||
/// last real proposal" is a single wall-clock fact the admin wants regardless of which historical window they
|
||||
/// are currently browsing. <see langword="null"/> only if no proposal has ever been created.
|
||||
/// </param>
|
||||
public record EvaluationHistorySummaryDto(
|
||||
[property: JsonPropertyName("totalEvaluations")] int TotalEvaluations,
|
||||
[property: JsonPropertyName("countsByOutcome")] List<OutcomeReasonCountDto> CountsByOutcome,
|
||||
[property: JsonPropertyName("averageCompositeScore")] decimal AverageCompositeScore,
|
||||
[property: JsonPropertyName("proposalsCreated")] int ProposalsCreated,
|
||||
[property: JsonPropertyName("lastProposalCreatedAtUtc")] System.DateTime? LastProposalCreatedAtUtc
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Full response for the evaluation-history RPC channel: a page of matching rows, the total match count (for
|
||||
/// pagination), and a pre-aggregated <see cref="Summary"/> so the Web UI never needs a second round trip (and a
|
||||
/// second, potentially-inconsistent set of filters) just to render a header line above the table.
|
||||
/// </summary>
|
||||
public record GetEvaluationHistoryResponse(
|
||||
[property: JsonPropertyName("totalCount")] int TotalCount,
|
||||
[property: JsonPropertyName("entries")] List<EvaluationHistoryEntryDto> Entries,
|
||||
[property: JsonPropertyName("summary")] EvaluationHistorySummaryDto Summary
|
||||
);
|
||||
@@ -0,0 +1,110 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace FinlyticCore.Dtos.Trading;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<ExecutionMode>))]
|
||||
public enum ExecutionMode
|
||||
{
|
||||
SignalProposal, // Reines Signal zur manuellen Ansicht
|
||||
ManualTradeRepublic, // Händisch bei Trade Republic ausgeführt
|
||||
PaperTradingBot // Vollautomatisch im Paper-Trading-Modus
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<TradeStatus>))]
|
||||
public enum TradeStatus
|
||||
{
|
||||
Proposed, // KI-geprüfter Vorschlag, wartet auf Ausführung
|
||||
Active, // Mindestens 1 Fill ausgeführt, Trade läuft
|
||||
BreakEvenTriggered, // Kurs hat TP1 erreicht, SL liegt auf Mischkurs
|
||||
Tp1Hit, // Teilverkauf 1 ausgeführt
|
||||
Tp2Hit, // Teilverkauf 2 ausgeführt
|
||||
Closed, // Vollständig mit Gewinn glattgestellt
|
||||
StoppedOut, // Durch Stop-Loss beendet
|
||||
Invalidated, // Kurs hat Invalidation erreicht, bevor Einstieg erfolgte
|
||||
Expired // Gültigkeitsfenster abgelaufen
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<InstrumentCategoryType>))]
|
||||
public enum InstrumentCategoryType
|
||||
{
|
||||
Stock,
|
||||
Etf,
|
||||
TurboLong,
|
||||
TurboShort,
|
||||
FactorCertificate
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies whether an <c>EngineEvaluationSnapshotEntity</c> row was produced by the autonomous
|
||||
/// <c>OpportunityPollerBackgroundService</c> scan loop or by an on-demand, human-initiated call (Web UI
|
||||
/// "Analyze now" / <c>EngineController.EvaluateAsset</c> / <c>AnalyzeController.TriggerManualAnalysis</c>).
|
||||
/// <see cref="Unknown"/> is deliberately value <c>0</c> (the default) so that snapshot rows written before
|
||||
/// this field existed - and any future row where the caller genuinely failed to specify a source - are never
|
||||
/// silently mis-reported as one of the two real sources (Rules.md §4: no fabricated data, an honest
|
||||
/// "we don't know" beats a fabricated default of <see cref="Automatic"/>).
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<TriggerSource>))]
|
||||
public enum TriggerSource
|
||||
{
|
||||
Unknown = 0,
|
||||
Automatic = 1,
|
||||
Manual = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classifies why a single asset evaluation in <c>TradeLifecycleService.EvaluateAssetAsync</c> did or did not
|
||||
/// result in a trade proposal. <see cref="Unknown"/> is deliberately value <c>0</c> (the default) so snapshot
|
||||
/// rows persisted before this field existed read honestly as "reason unknown" rather than fabricating a
|
||||
/// specific-looking cause (Rules.md §4). See the "DetermineOutcomeReason" doc comment in
|
||||
/// <c>TradeLifecycleService</c> for the exact priority order applied when more than one gate failed at once.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<OutcomeReason>))]
|
||||
public enum OutcomeReason
|
||||
{
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>The AI reasoning gate approved the opportunity and a <c>EngineTradeProposalEntity</c> was created.</summary>
|
||||
Approved = 1,
|
||||
|
||||
/// <summary>
|
||||
/// <c>ScoringResult.CompositeScore</c> stayed below <c>Engine.MinCompositeScore</c> and the evaluation was
|
||||
/// not forced, so the AI reasoning gate was never even consulted (a synthetic rule-based rejection was
|
||||
/// recorded instead).
|
||||
/// </summary>
|
||||
BelowScoreThreshold = 2,
|
||||
|
||||
/// <summary>The asset is within the earnings blackout window (<c>Engine.EarningsLockoutDays</c>).</summary>
|
||||
EarningsLockout = 3,
|
||||
|
||||
/// <summary>FinlyticSimulation's backtest-reliability matrix vetoed this strategy/asset combination.</summary>
|
||||
SimulationVeto = 4,
|
||||
|
||||
/// <summary>
|
||||
/// The composite score cleared the minimum threshold (or the evaluation was forced) and neither the
|
||||
/// earnings-lockout nor the simulation-veto gate blocked it, but the AI reasoning gate itself - whether a
|
||||
/// real AI call or one of its own rule-based fallbacks (gate disabled, webhook unreachable) - still declined.
|
||||
/// </summary>
|
||||
AiRejected = 5,
|
||||
|
||||
/// <summary>
|
||||
/// No technical setup could be produced for the ISIN at all (FinlyticTechnicals returned nothing), or the
|
||||
/// ISIN itself was blank - in both cases the pipeline never reached scoring, so every score field on the
|
||||
/// snapshot is <c>0</c>/default rather than fabricated.
|
||||
/// </summary>
|
||||
NoTechnicalSetups = 6,
|
||||
|
||||
/// <summary>
|
||||
/// Not a real rejection: the evaluation genuinely cleared every gate and the AI reasoning gate approved the
|
||||
/// opportunity (<c>PassedAiValidation</c> on this same row is <see langword="true"/>), but an active,
|
||||
/// non-expired <c>EngineTradeProposalEntity</c> for the same <c>UnderlyingIsin</c> already exists, so no
|
||||
/// second, near-identical proposal row was created and no <c>finlytic/engine/proposals/created</c> MQTT
|
||||
/// event was re-broadcast. Exists specifically to stop the autonomous scanner from spamming a fresh
|
||||
/// proposal (and a fresh push event to every connected client) every single poll cycle for as long as one
|
||||
/// asset stays above the approval threshold - the underlying bug this value was introduced to fix.
|
||||
/// </summary>
|
||||
DuplicateActiveProposal = 7,
|
||||
|
||||
/// <summary>The asset is within the ex-dividend blackout window (<c>Engine.DividendGateDays</c>).</summary>
|
||||
DividendGate = 8
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user