feat(core): add shared DTOs, MqttTopics constants, DatabaseBootstrapper, and ManagedMqttClient extensions
This commit is contained in:
@@ -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
|
||||
);
|
||||
Reference in New Issue
Block a user