Files

303 lines
14 KiB
C#

using System.Text.Json.Serialization;
namespace FinlyticCore.Dtos;
/// <summary>
/// Generic request payload carrying only a limit parameter (e.g. news_GetPending).
/// </summary>
public record LimitRequest(
[property: JsonPropertyName("limit")] int Limit
);
/// <summary>
/// Generic request payload for paginated queries with optional ISIN filter.
/// </summary>
public record PaginatedRequest(
[property: JsonPropertyName("limit")] int Limit,
[property: JsonPropertyName("offset")] int Offset,
[property: JsonPropertyName("isin")] string? Isin = null
);
/// <summary>
/// Request payload for daily-news queries with optional filters.
/// </summary>
public record DailyNewsRequest(
[property: JsonPropertyName("limit")] int Limit,
[property: JsonPropertyName("offset")] int Offset,
[property: JsonPropertyName("isin")] string? Isin = null,
[property: JsonPropertyName("date")] DateTime? Date = null,
[property: JsonPropertyName("status")] string? Status = null,
[property: JsonPropertyName("query")] string? Query = null,
[property: JsonPropertyName("hasSentiment")]
bool? HasSentiment = null
);
/// <summary>
/// Request payload for fetching fundamentals or technical-analysis data by ISIN.
/// </summary>
public record IsinRequest(
[property: JsonPropertyName("isin")] string Isin,
[property: JsonPropertyName("ticker")] string? Ticker = "",
[property: JsonPropertyName("forceRefresh")]
bool ForceRefresh = false
);
/// <summary>
/// Request payload for fetching trades filtered by ISIN and/or status.
/// </summary>
public record GetTradesRequest(
[property: JsonPropertyName("isin")] string? Isin = null,
[property: JsonPropertyName("status")] string? Status = null,
[property: JsonPropertyName("userId")] string? UserId = null
);
/// <summary>
/// Request payload for fetching sentiment by article ID.
/// </summary>
public record ArticleRequest(
[property: JsonPropertyName("articleId")]
string ArticleId,
[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>
public record AnalyzeSentimentRequest(
[property: JsonPropertyName("articleId")]
string? ArticleId = null,
[property: JsonPropertyName("isin")] string? Isin = null,
[property: JsonPropertyName("forceReload")]
bool ForceReload = false
);
/// <summary>
/// Empty request payload for MQTT RPCs that require no parameters (e.g. events_GetAll).
/// </summary>
public record EmptyRequest;
/// <summary>
/// Request payload for paginated/monthly calendar queries.
/// </summary>
public record GetEventsByMonthRequest(
[property: JsonPropertyName("year")] int Year,
[property: JsonPropertyName("month")] int Month
);
/// <summary>
/// Response payload returned by microservice health pings over MQTT.
/// </summary>
public record ServiceHealthResponse(
[property: JsonPropertyName("serviceName")]
string ServiceName,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("timestamp")]
DateTime Timestamp,
[property: JsonPropertyName("dbStatus")]
string DbStatus
);
/// <summary>
/// Response payload for assets_FetchLogo RPC request.
/// </summary>
public record FetchLogoResponse(
[property: JsonPropertyName("isin")] string? Isin,
[property: JsonPropertyName("path")] string? Path,
[property: JsonPropertyName("success")]
bool Success
);
/// <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>
/// Generic request payload carrying a user GUID to resolve user identity across microservices.
/// </summary>
public record UserIdRequest(
[property: JsonPropertyName("userId")] Guid UserId
);
/// <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
);