Observable, caller-held execution saga for approved option plans.
new/2 converts a fresh %Bourse.OptionProposal.Result{status: :approved}
into a journal with stable plan, leg, and client-order identifiers.
step/3 is pure: it returns at most one command and the journal state that
must be retained before that command is executed. execute/3 performs that
single effect. resume/4 is pure and records the exact observed outcome.
A submission with an ambiguous outcome becomes :unknown. The next command
is reconciliation, never another submission. Definite failures stop later
legs and switch to cancellation of reversible acknowledged remainders.
Filled exposure is reported as residual risk requiring caller action; it is
never described as rolled back.
Summary
Functions
Executes exactly one command against caller-supplied exchange configurations.
Creates a journal from a fresh approved preflight result.
Records one command outcome in the caller-held journal.
Emits the next command and the journal state that precedes its execution.
Types
@type command() :: %{ :id => String.t(), :action => command_action(), :plan_id => String.t(), :approval_id => String.t(), :leg_id => term(), :execution_id => String.t(), :client_order_id => String.t(), :venue => String.t(), :account => term(), :symbol => String.t(), :type => String.t(), :side => String.t(), :amount => number(), optional(:price) => number() | nil, optional(:order_id) => String.t() | nil }
@type command_action() :: :submit | :reconcile | :cancel
@type outcome() :: Bourse.OptionSaga.Executor.outcome()
Functions
@spec execute(command(), Bourse.OptionSaga.Executor.exchanges(), keyword()) :: outcome()
Executes exactly one command against caller-supplied exchange configurations.
@spec new( Bourse.OptionProposal.Result.t(), keyword() ) :: {:ok, Bourse.OptionSaga.Journal.t()} | {:error, Bourse.Error.t()}
Creates a journal from a fresh approved preflight result.
Options:
:now_ms— current time override:max_approval_age_ms— maximum age before a new submission is refused
@spec resume(Bourse.OptionSaga.Journal.t(), command(), outcome(), keyword()) :: {:ok, Bourse.OptionSaga.Journal.t()} | {:error, Bourse.Error.t()}
Records one command outcome in the caller-held journal.
Replaying the same command outcome is idempotent. An outcome for another plan, approval, or leg is rejected.
@spec step(Bourse.OptionSaga.Journal.t(), Bourse.OptionProposal.Result.t(), keyword()) :: {:ok, command(), Bourse.OptionSaga.Journal.t()} | {:done, Bourse.OptionSaga.Journal.t()} | {:halted, Bourse.OptionSaga.Journal.t()} | {:error, Bourse.Error.t()}
Emits the next command and the journal state that precedes its execution.
New submissions re-check both approval identity and freshness before a command is returned. Reconciliation and compensation remain available after approval expiry because they reduce uncertainty or risk rather than add a new planned exposure.