using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FinlyticCore.Dtos; using FinlyticCore.Dtos.Trading; namespace FinlyticEngine.Services.Trading; /// /// Coordinates the full trade proposal/trade lifecycle for FinlyticEngine: on-demand evaluation, proposal /// acceptance/rejection, and management of the resulting active trades (fills, stop-loss updates, closes). /// public interface ITradeLifecycleService { /// /// Returns trade proposals, optionally restricted to still-active, non-expired ones. /// Task> GetProposalsAsync(bool onlyActive = true, int limit = 50, CancellationToken cancellationToken = default); /// /// Returns the active trades owned by , optionally filtered by /// . The filter is applied in the database, so another user's trades are never /// materialised and a caller cannot widen the result set by omitting a parameter. /// Task> GetActiveTradesAsync(Guid userId, ExecutionMode? mode = null, CancellationToken cancellationToken = default); /// /// Runs the full multi-factor evaluation pipeline (technicals, sentiment, fundamentals, simulation feedback, /// AI reasoning gate) for a single ISIN and persists a new if the opportunity /// is approved. Unlike the old TradeProposalDto? contract, this never returns : /// a rejection (score too low, or the AI gate declined) is reported as an /// with Proposal == null but with the real, already-computed /// scores and AI reasoning filled in, so a caller always learns *why*, not just *that* no proposal was made /// (Rules.md §4). When not even a technical setup could be found for the ISIN, the score fields are 0 /// and carries a "[Regelbasiert]"-prefixed /// explanation rather than a fabricated AI verdict. /// /// Every call - including the early "no technical setup"/"blank ISIN" returns - now persists exactly one /// EngineEvaluationSnapshotEntity row tagged with (and /// when is /// ), so the admin evaluation-history tab /// (MqttTopics.Channels.EngineGetEvaluationHistory) can account for every asset this pipeline ever /// looked at, not only the ones that made it all the way to scoring. /// /// /// An approval that would otherwise create a second for an ISIN that /// already has an active, non-expired proposal is deduplicated: no new proposal row is created and no /// finlytic/engine/proposals/created event is re-broadcast, the persisted snapshot's /// OutcomeReason is instead of /// , and the returned is /// the pre-existing proposal (never ) so a caller still learns about the open /// opportunity. This exists because the autonomous scanner re-evaluates the same top-picks every cycle and /// would otherwise create a near-identical proposal (and broadcast) for as long as one asset stays above /// the approval threshold. /// /// /// The underlying ISIN to evaluate. /// Optional ticker hint passed through to the technical/fundamentals lookups. /// /// When , the AI reasoning gate is consulted even if the composite score is below /// Engine.MinCompositeScore (used by the manual "Analyze now" Web UI flow). /// /// /// Whether this call originates from the autonomous OpportunityPollerBackgroundService scan loop /// (, the default) or an on-demand human request /// (). /// /// /// The identity of the human caller when is . /// Must be for calls - the autonomous scanner /// never carries a user identity, and this is enforced defensively regardless of what is passed in. /// /// Propagated to every downstream MQTT/DB call. Task EvaluateAssetAsync( string isin, string? ticker = null, bool forceAiEvaluation = false, TriggerSource triggerSource = TriggerSource.Automatic, Guid? triggeredByUserId = null, CancellationToken cancellationToken = default); /// /// Records an additional executed fill against an existing active trade and recalculates its average /// buy-in, total quantity, fees, and dynamic take-profit levels. /// /// /// Thrown when no trade with exists for . A trade owned /// by a different user is reported the same way as a missing one, so ownership is never disclosed. /// Task AddTradeFillAsync(Guid userId, Guid tradeId, decimal executedPrice, decimal quantity, decimal fee = 0m, string? note = null, CancellationToken cancellationToken = default); /// /// Manually or algorithmically adjusts the stop-loss of an active trade owned by . /// /// /// Thrown when no trade with exists for . /// Task UpdateStopLossAsync(Guid userId, Guid tradeId, decimal newStopLoss, string reason, CancellationToken cancellationToken = default); /// /// Closes an active trade owned by at the given price and computes its realized P&L. /// /// /// Thrown when no trade with exists for . /// Task CloseTradeAsync(Guid userId, Guid tradeId, decimal closePrice, string reason, CancellationToken cancellationToken = default); /// /// Creates an actively tracked EngineTradeEntity owned by from an open /// proposal. The source proposal is deliberately left active: a proposal is a system-wide opportunity that /// several users may accept independently, each receiving their own trade. Proposals are not consumed by /// acceptance — they disappear on their own once ExpiresAtUtc passes. /// /// if no active, non-expired proposal with exists. /// /// Thrown when already holds a trade created from this proposal. /// Task CreateTradeFromProposalAsync(Guid userId, Guid proposalId, ExecutionMode mode, decimal? initialFillPrice = null, decimal? initialQuantity = null, CancellationToken cancellationToken = default); /// /// Accepts a proposal on behalf of a single user via the engine_AcceptProposal MQTT RPC channel. /// Thin wrapper around — see there for the ownership and /// non-consumption semantics. Declining a proposal deliberately has no counterpart here: it has no /// server-side effect and is handled entirely in the client. /// /// /// The proposal does not exist, has expired, or this user already accepted it. /// Task AcceptProposalAsync(AcceptTradeProposalRequest request, CancellationToken cancellationToken = default); /// /// Opens an actively tracked trade owned by request.UserId with no backing proposal (manual entry, /// e.g. from the Web UI). Unlike , the resulting /// EngineTradeEntity.ProposalId is since there is no proposal to link to. /// /// /// UnderlyingIsin/Symbol is blank, or EntryPrice/Quantity is not positive. /// Task CreateManualTradeAsync(CreateManualTradeRequest request, CancellationToken cancellationToken = default); }