Skip to main content

qs_core/
execution_events.rs

1use std::fmt;
2use std::marker::PhantomData;
3use std::num::NonZeroU32;
4use std::str::FromStr;
5
6use qs_instruments::{AssetId, Decimal, ExecutionVenueId, InstrumentId, Price, Quantity};
7use serde::de::{DeserializeOwned, SeqAccess, Visitor};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use crate::canonical::{
11    DateTimeUtc, ExecutionCommandId, FillId, OpaquePayloadRef, TradeIntentId, VenueOrderRef,
12    VenuePositionRef,
13};
14use crate::types::{CloseReason, Effect, FutureEffect, Side};
15
16pub const EXECUTION_SCHEMA_VERSION: u32 = 1;
17pub const MAX_BOUNDED_TEXT_BYTES: usize = 512;
18pub const MAX_VENUE_SEQUENCE_BYTES: usize = 160;
19pub const MAX_REPORT_NAMESPACE_BYTES: usize = 96;
20pub const MAX_FILL_FEES: usize = 32;
21pub const MAX_POSITION_TARGETS: usize = 64;
22pub const MAX_CLOSE_FEES: usize = 32;
23
24#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
25pub enum ExecutionEventError {
26    #[error("schema version must be {EXECUTION_SCHEMA_VERSION}, got {0}")]
27    InvalidSchemaVersion(u32),
28    #[error("{kind} length must be between 1 and {maximum} bytes, got {actual}")]
29    InvalidTextLength {
30        kind: &'static str,
31        maximum: usize,
32        actual: usize,
33    },
34    #[error("{kind} contains an unsupported character")]
35    InvalidTextCharacter { kind: &'static str },
36    #[error("{kind} quantity must be positive")]
37    QuantityMustBePositive { kind: &'static str },
38    #[error("fee amount must be nonzero")]
39    ZeroFeeAmount,
40    #[error("{kind} count exceeds maximum {maximum}, got {actual}")]
41    CollectionTooLarge {
42        kind: &'static str,
43        maximum: usize,
44        actual: usize,
45    },
46    #[error("order cumulative and remaining quantities must equal order quantity")]
47    InconsistentOrderQuantities,
48    #[error("fill cumulative quantity cannot be smaller than incremental quantity")]
49    InconsistentFillQuantities,
50    #[error("partial fill must have a positive remaining quantity")]
51    PartialFillWithoutRemainingQuantity,
52    #[error("final fill must have zero remaining quantity")]
53    FinalFillWithRemainingQuantity,
54    #[error("fill and order snapshots do not describe the same order state")]
55    FillOrderMismatch,
56    #[error("event and order snapshot references do not match")]
57    OrderReferenceMismatch,
58    #[error("reconciliation must include an order or position snapshot")]
59    EmptyReconciliation,
60    #[error("partial close ratio must be finite, greater than zero, and less than one")]
61    InvalidPartialCloseRatio,
62    #[error("report counter overflow")]
63    ReportCounterOverflow,
64    #[error("invalid exact decimal value: {0}")]
65    InvalidDecimal(String),
66    #[error("invalid canonical identity: {0}")]
67    InvalidIdentity(String),
68}
69
70fn validate_text(
71    kind: &'static str,
72    value: &str,
73    maximum: usize,
74    ascii_only: bool,
75    reject_whitespace: bool,
76) -> Result<(), ExecutionEventError> {
77    if value.is_empty() || value.len() > maximum {
78        return Err(ExecutionEventError::InvalidTextLength {
79            kind,
80            maximum,
81            actual: value.len(),
82        });
83    }
84    if (ascii_only && !value.is_ascii())
85        || value.bytes().any(|byte| {
86            byte.is_ascii_control() || (reject_whitespace && byte.is_ascii_whitespace())
87        })
88    {
89        return Err(ExecutionEventError::InvalidTextCharacter { kind });
90    }
91    Ok(())
92}
93
94macro_rules! bounded_string {
95    ($name:ident, $kind:literal, $maximum:expr, $ascii_only:expr, $reject_whitespace:expr) => {
96        #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
97        pub struct $name(String);
98
99        impl $name {
100            pub fn new(value: impl Into<String>) -> Result<Self, ExecutionEventError> {
101                let value = value.into();
102                validate_text($kind, &value, $maximum, $ascii_only, $reject_whitespace)?;
103                Ok(Self(value))
104            }
105
106            pub fn as_str(&self) -> &str {
107                &self.0
108            }
109        }
110
111        impl fmt::Display for $name {
112            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113                formatter.write_str(&self.0)
114            }
115        }
116
117        impl FromStr for $name {
118            type Err = ExecutionEventError;
119
120            fn from_str(value: &str) -> Result<Self, Self::Err> {
121                Self::new(value)
122            }
123        }
124
125        impl Serialize for $name {
126            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
127            where
128                S: Serializer,
129            {
130                serializer.serialize_str(&self.0)
131            }
132        }
133
134        impl<'de> Deserialize<'de> for $name {
135            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
136            where
137                D: Deserializer<'de>,
138            {
139                Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
140            }
141        }
142    };
143}
144
145bounded_string!(
146    BoundedText,
147    "bounded text",
148    MAX_BOUNDED_TEXT_BYTES,
149    false,
150    false
151);
152bounded_string!(
153    VenueSequence,
154    "venue sequence",
155    MAX_VENUE_SEQUENCE_BYTES,
156    true,
157    true
158);
159bounded_string!(
160    ReportNamespace,
161    "report namespace",
162    MAX_REPORT_NAMESPACE_BYTES,
163    true,
164    true
165);
166
167fn deserialize_schema_version<'de, D>(deserializer: D) -> Result<u32, D::Error>
168where
169    D: Deserializer<'de>,
170{
171    let version = u32::deserialize(deserializer)?;
172    if version != EXECUTION_SCHEMA_VERSION {
173        return Err(serde::de::Error::custom(
174            ExecutionEventError::InvalidSchemaVersion(version),
175        ));
176    }
177    Ok(version)
178}
179
180fn deserialize_bounded_vec<'de, D, T, const MAXIMUM: usize>(
181    deserializer: D,
182    collection: &'static str,
183) -> Result<Vec<T>, D::Error>
184where
185    D: Deserializer<'de>,
186    T: DeserializeOwned,
187{
188    struct BoundedVecVisitor<T, const MAXIMUM: usize> {
189        collection: &'static str,
190        marker: PhantomData<T>,
191    }
192
193    impl<'de, T, const MAXIMUM: usize> Visitor<'de> for BoundedVecVisitor<T, MAXIMUM>
194    where
195        T: DeserializeOwned,
196    {
197        type Value = Vec<T>;
198
199        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
200            write!(formatter, "at most {MAXIMUM} values in {}", self.collection)
201        }
202
203        fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
204        where
205            A: SeqAccess<'de>,
206        {
207            if sequence.size_hint().is_some_and(|size| size > MAXIMUM) {
208                return Err(serde::de::Error::custom(format!(
209                    "{} exceeds maximum length {MAXIMUM}",
210                    self.collection
211                )));
212            }
213            let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0).min(MAXIMUM));
214            while let Some(value) = sequence.next_element()? {
215                if values.len() == MAXIMUM {
216                    return Err(serde::de::Error::custom(format!(
217                        "{} exceeds maximum length {MAXIMUM}",
218                        self.collection
219                    )));
220                }
221                values.push(value);
222            }
223            Ok(values)
224        }
225    }
226
227    deserializer.deserialize_seq(BoundedVecVisitor::<T, MAXIMUM> {
228        collection,
229        marker: PhantomData,
230    })
231}
232
233fn deserialize_fill_fees<'de, D>(deserializer: D) -> Result<Vec<FeeAmount>, D::Error>
234where
235    D: Deserializer<'de>,
236{
237    deserialize_bounded_vec::<D, _, MAX_FILL_FEES>(deserializer, "fill fees")
238}
239
240fn deserialize_position_targets<'de, D>(deserializer: D) -> Result<Vec<CanonicalTarget>, D::Error>
241where
242    D: Deserializer<'de>,
243{
244    deserialize_bounded_vec::<D, _, MAX_POSITION_TARGETS>(deserializer, "position targets")
245}
246
247fn deserialize_close_fees<'de, D>(deserializer: D) -> Result<Vec<FeeAmount>, D::Error>
248where
249    D: Deserializer<'de>,
250{
251    deserialize_bounded_vec::<D, _, MAX_CLOSE_FEES>(deserializer, "close fees")
252}
253
254fn deserialize_positive_quantity<'de, D>(deserializer: D) -> Result<Quantity, D::Error>
255where
256    D: Deserializer<'de>,
257{
258    let quantity = Quantity::deserialize(deserializer)?;
259    require_positive_quantity("canonical", quantity).map_err(serde::de::Error::custom)?;
260    Ok(quantity)
261}
262
263fn deserialize_nonzero_decimal<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
264where
265    D: Deserializer<'de>,
266{
267    let amount = Decimal::deserialize(deserializer)?;
268    if amount.is_zero() {
269        return Err(serde::de::Error::custom(ExecutionEventError::ZeroFeeAmount));
270    }
271    Ok(amount)
272}
273
274fn require_positive_quantity(
275    kind: &'static str,
276    quantity: Quantity,
277) -> Result<(), ExecutionEventError> {
278    quantity
279        .require_positive()
280        .map(|_| ())
281        .map_err(|_| ExecutionEventError::QuantityMustBePositive { kind })
282}
283
284fn validate_collection_bound(
285    kind: &'static str,
286    actual: usize,
287    maximum: usize,
288) -> Result<(), ExecutionEventError> {
289    if actual > maximum {
290        return Err(ExecutionEventError::CollectionTooLarge {
291            kind,
292            maximum,
293            actual,
294        });
295    }
296    Ok(())
297}
298
299mod side_serde {
300    use super::*;
301
302    pub fn serialize<S>(side: &Side, serializer: S) -> Result<S::Ok, S::Error>
303    where
304        S: Serializer,
305    {
306        serializer.serialize_str(match side {
307            Side::Buy => "buy",
308            Side::Sell => "sell",
309        })
310    }
311
312    pub fn deserialize<'de, D>(deserializer: D) -> Result<Side, D::Error>
313    where
314        D: Deserializer<'de>,
315    {
316        match String::deserialize(deserializer)?.as_str() {
317            "buy" => Ok(Side::Buy),
318            "sell" => Ok(Side::Sell),
319            value => Err(serde::de::Error::unknown_variant(value, &["buy", "sell"])),
320        }
321    }
322}
323
324#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
325#[serde(deny_unknown_fields)]
326pub struct ExecutionCommandEnvelope<T> {
327    #[serde(deserialize_with = "deserialize_schema_version")]
328    pub schema_version: u32,
329    pub command_id: ExecutionCommandId,
330    pub intent_id: TradeIntentId,
331    pub created_at: DateTimeUtc,
332    pub payload: T,
333}
334
335impl<T> ExecutionCommandEnvelope<T> {
336    pub fn new(
337        command_id: ExecutionCommandId,
338        intent_id: TradeIntentId,
339        created_at: DateTimeUtc,
340        payload: T,
341    ) -> Self {
342        Self {
343            schema_version: EXECUTION_SCHEMA_VERSION,
344            command_id,
345            intent_id,
346            created_at,
347            payload,
348        }
349    }
350
351    pub fn with_deterministic_id(
352        intent_id: TradeIntentId,
353        command_ordinal: u64,
354        created_at: DateTimeUtc,
355        payload: T,
356    ) -> Self {
357        let command_id = deterministic_execution_command_id(&intent_id, command_ordinal);
358        Self::new(command_id, intent_id, created_at, payload)
359    }
360}
361
362impl<T: PartialEq> ExecutionCommandEnvelope<T> {
363    pub fn compare(&self, other: &Self) -> CommandComparison {
364        if self.command_id != other.command_id {
365            CommandComparison::Distinct
366        } else if self == other {
367            CommandComparison::Duplicate
368        } else {
369            CommandComparison::Conflict
370        }
371    }
372}
373
374fn deterministic_execution_command_id(
375    intent_id: &TradeIntentId,
376    command_ordinal: u64,
377) -> ExecutionCommandId {
378    ExecutionCommandId::new(format!("command:{intent_id}:{command_ordinal}"))
379        .expect("deterministic execution command ID is valid")
380}
381
382#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
383pub enum CommandComparison {
384    Distinct,
385    Duplicate,
386    Conflict,
387}
388
389#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
390#[serde(deny_unknown_fields)]
391pub struct CommandDispatchAttempt<T> {
392    pub command: ExecutionCommandEnvelope<T>,
393    pub attempt: NonZeroU32,
394    pub dispatched_at: DateTimeUtc,
395}
396
397impl<T> CommandDispatchAttempt<T> {
398    pub fn new(
399        command: ExecutionCommandEnvelope<T>,
400        attempt: NonZeroU32,
401        dispatched_at: DateTimeUtc,
402    ) -> Self {
403        Self {
404            command,
405            attempt,
406            dispatched_at,
407        }
408    }
409}
410
411#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
412#[serde(rename_all = "snake_case")]
413pub enum DispatchFailureCategory {
414    Timeout,
415    Connection,
416    Authentication,
417    Authorization,
418    RateLimited,
419    Serialization,
420    Protocol,
421    GatewayUnavailable,
422    Internal,
423    Other,
424}
425
426#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
427#[serde(
428    tag = "type",
429    content = "data",
430    rename_all = "snake_case",
431    deny_unknown_fields
432)]
433pub enum CommandDispatchEvent {
434    TransportAcknowledged {
435        gateway_reference: Option<BoundedText>,
436    },
437    TransportFailed {
438        category: DispatchFailureCategory,
439        message: BoundedText,
440    },
441    UnknownOutcome {
442        category: DispatchFailureCategory,
443        message: BoundedText,
444    },
445    ReconciliationRequired {
446        reason: BoundedText,
447    },
448}
449
450#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
451#[serde(deny_unknown_fields)]
452pub struct CommandDispatchReport {
453    #[serde(deserialize_with = "deserialize_schema_version")]
454    pub schema_version: u32,
455    pub intent_id: TradeIntentId,
456    pub command_id: ExecutionCommandId,
457    pub execution_venue: ExecutionVenueId,
458    pub observed_at: DateTimeUtc,
459    pub event: CommandDispatchEvent,
460}
461
462impl CommandDispatchReport {
463    pub fn new(
464        intent_id: TradeIntentId,
465        command_id: ExecutionCommandId,
466        execution_venue: ExecutionVenueId,
467        observed_at: DateTimeUtc,
468        event: CommandDispatchEvent,
469    ) -> Self {
470        Self {
471            schema_version: EXECUTION_SCHEMA_VERSION,
472            intent_id,
473            command_id,
474            execution_venue,
475            observed_at,
476            event,
477        }
478    }
479
480    pub fn compare(&self, other: &Self) -> ReportComparison {
481        let same_identity = self.intent_id == other.intent_id
482            && self.command_id == other.command_id
483            && self.execution_venue == other.execution_venue
484            && self.observed_at == other.observed_at;
485        compare_report_parts(same_identity, self == other)
486    }
487}
488
489#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
490#[serde(rename_all = "snake_case")]
491pub enum LiquidityRole {
492    Maker,
493    Taker,
494    Auction,
495    Internalized,
496    Unknown,
497}
498
499#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
500#[serde(rename_all = "snake_case")]
501pub enum FeeType {
502    Commission,
503    Exchange,
504    Clearing,
505    Regulatory,
506    Financing,
507    Tax,
508    Rebate,
509    Other,
510}
511
512#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
513#[serde(rename_all = "snake_case")]
514pub enum RejectionCategory {
515    InvalidOrder,
516    InsufficientFunds,
517    RiskLimit,
518    MarketClosed,
519    InstrumentUnavailable,
520    PriceOutOfRange,
521    QuantityOutOfRange,
522    Duplicate,
523    PermissionDenied,
524    VenueUnavailable,
525    Other,
526}
527
528#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
529#[serde(rename_all = "snake_case")]
530pub enum ReconciliationSource {
531    VenueSnapshot,
532    VenueHistory,
533    DropCopy,
534    Replay,
535    PaperLedger,
536    Operator,
537}
538
539#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
540#[serde(rename_all = "snake_case")]
541pub enum CanonicalCloseReason {
542    StopLoss,
543    Target,
544    TrailingStop,
545    TimeExit,
546    BreakevenStop,
547    Manual,
548    EndOfData,
549    GroupRule,
550    Cancelled,
551    Liquidation,
552    Venue,
553    Reconciliation,
554    Other,
555}
556
557/// Signed nonzero amount for a charge or rebate. Positive values debit the account and negative values credit it.
558#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
559#[serde(deny_unknown_fields)]
560pub struct FeeAmount {
561    pub asset: AssetId,
562    #[serde(deserialize_with = "deserialize_nonzero_decimal")]
563    pub amount: Decimal,
564    pub fee_type: FeeType,
565}
566
567impl FeeAmount {
568    pub fn new(
569        asset: AssetId,
570        amount: Decimal,
571        fee_type: FeeType,
572    ) -> Result<Self, ExecutionEventError> {
573        if amount.is_zero() {
574            return Err(ExecutionEventError::ZeroFeeAmount);
575        }
576        Ok(Self {
577            asset,
578            amount,
579            fee_type,
580        })
581    }
582
583    fn validate(&self) -> Result<(), ExecutionEventError> {
584        if self.amount.is_zero() {
585            return Err(ExecutionEventError::ZeroFeeAmount);
586        }
587        Ok(())
588    }
589}
590
591#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
592#[serde(deny_unknown_fields)]
593pub struct CanonicalOrderSnapshot {
594    pub venue_order_ref: VenueOrderRef,
595    #[serde(with = "side_serde")]
596    pub side: Side,
597    pub order_quantity: Quantity,
598    pub cumulative_quantity: Quantity,
599    pub remaining_quantity: Quantity,
600    pub average_fill_price: Option<Price>,
601}
602
603#[derive(Deserialize)]
604#[serde(deny_unknown_fields)]
605struct CanonicalOrderSnapshotWire {
606    venue_order_ref: VenueOrderRef,
607    #[serde(with = "side_serde")]
608    side: Side,
609    #[serde(deserialize_with = "deserialize_positive_quantity")]
610    order_quantity: Quantity,
611    cumulative_quantity: Quantity,
612    remaining_quantity: Quantity,
613    average_fill_price: Option<Price>,
614}
615
616impl CanonicalOrderSnapshot {
617    pub fn new(
618        venue_order_ref: VenueOrderRef,
619        side: Side,
620        order_quantity: Quantity,
621        cumulative_quantity: Quantity,
622        remaining_quantity: Quantity,
623        average_fill_price: Option<Price>,
624    ) -> Result<Self, ExecutionEventError> {
625        require_positive_quantity("order", order_quantity)?;
626        let total = cumulative_quantity
627            .get()
628            .checked_add(remaining_quantity.get())
629            .map_err(|error| ExecutionEventError::InvalidDecimal(error.to_string()))?;
630        if total != order_quantity.get() {
631            return Err(ExecutionEventError::InconsistentOrderQuantities);
632        }
633        Ok(Self {
634            venue_order_ref,
635            side,
636            order_quantity,
637            cumulative_quantity,
638            remaining_quantity,
639            average_fill_price,
640        })
641    }
642
643    fn validate(&self) -> Result<(), ExecutionEventError> {
644        require_positive_quantity("order", self.order_quantity)?;
645        let total = self
646            .cumulative_quantity
647            .get()
648            .checked_add(self.remaining_quantity.get())
649            .map_err(|error| ExecutionEventError::InvalidDecimal(error.to_string()))?;
650        if total != self.order_quantity.get() {
651            return Err(ExecutionEventError::InconsistentOrderQuantities);
652        }
653        Ok(())
654    }
655}
656
657impl TryFrom<CanonicalOrderSnapshotWire> for CanonicalOrderSnapshot {
658    type Error = ExecutionEventError;
659
660    fn try_from(value: CanonicalOrderSnapshotWire) -> Result<Self, Self::Error> {
661        Self::new(
662            value.venue_order_ref,
663            value.side,
664            value.order_quantity,
665            value.cumulative_quantity,
666            value.remaining_quantity,
667            value.average_fill_price,
668        )
669    }
670}
671
672impl<'de> Deserialize<'de> for CanonicalOrderSnapshot {
673    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
674    where
675        D: Deserializer<'de>,
676    {
677        CanonicalOrderSnapshotWire::deserialize(deserializer)?
678            .try_into()
679            .map_err(serde::de::Error::custom)
680    }
681}
682
683#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
684#[serde(deny_unknown_fields)]
685pub struct CanonicalFill {
686    pub fill_id: Option<FillId>,
687    pub venue_order_ref: VenueOrderRef,
688    #[serde(with = "side_serde")]
689    pub side: Side,
690    pub price: Price,
691    pub quantity: Quantity,
692    pub cumulative_quantity: Quantity,
693    pub remaining_quantity: Quantity,
694    pub liquidity_role: LiquidityRole,
695    pub fees: Vec<FeeAmount>,
696}
697
698#[derive(Deserialize)]
699#[serde(deny_unknown_fields)]
700struct CanonicalFillWire {
701    fill_id: Option<FillId>,
702    venue_order_ref: VenueOrderRef,
703    #[serde(with = "side_serde")]
704    side: Side,
705    price: Price,
706    #[serde(deserialize_with = "deserialize_positive_quantity")]
707    quantity: Quantity,
708    #[serde(deserialize_with = "deserialize_positive_quantity")]
709    cumulative_quantity: Quantity,
710    remaining_quantity: Quantity,
711    liquidity_role: LiquidityRole,
712    #[serde(deserialize_with = "deserialize_fill_fees")]
713    fees: Vec<FeeAmount>,
714}
715
716impl CanonicalFill {
717    #[allow(clippy::too_many_arguments)]
718    pub fn new(
719        fill_id: Option<FillId>,
720        venue_order_ref: VenueOrderRef,
721        side: Side,
722        price: Price,
723        quantity: Quantity,
724        cumulative_quantity: Quantity,
725        remaining_quantity: Quantity,
726        liquidity_role: LiquidityRole,
727        fees: Vec<FeeAmount>,
728    ) -> Result<Self, ExecutionEventError> {
729        let value = Self {
730            fill_id,
731            venue_order_ref,
732            side,
733            price,
734            quantity,
735            cumulative_quantity,
736            remaining_quantity,
737            liquidity_role,
738            fees,
739        };
740        value.validate()?;
741        Ok(value)
742    }
743
744    fn validate(&self) -> Result<(), ExecutionEventError> {
745        require_positive_quantity("fill", self.quantity)?;
746        require_positive_quantity("cumulative fill", self.cumulative_quantity)?;
747        if self.cumulative_quantity < self.quantity {
748            return Err(ExecutionEventError::InconsistentFillQuantities);
749        }
750        validate_collection_bound("fill fee", self.fees.len(), MAX_FILL_FEES)?;
751        for fee in &self.fees {
752            fee.validate()?;
753        }
754        Ok(())
755    }
756}
757
758impl TryFrom<CanonicalFillWire> for CanonicalFill {
759    type Error = ExecutionEventError;
760
761    fn try_from(value: CanonicalFillWire) -> Result<Self, Self::Error> {
762        Self::new(
763            value.fill_id,
764            value.venue_order_ref,
765            value.side,
766            value.price,
767            value.quantity,
768            value.cumulative_quantity,
769            value.remaining_quantity,
770            value.liquidity_role,
771            value.fees,
772        )
773    }
774}
775
776impl<'de> Deserialize<'de> for CanonicalFill {
777    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
778    where
779        D: Deserializer<'de>,
780    {
781        CanonicalFillWire::deserialize(deserializer)?
782            .try_into()
783            .map_err(serde::de::Error::custom)
784    }
785}
786
787#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
788#[serde(deny_unknown_fields)]
789pub struct CanonicalProtection {
790    pub stop_loss: Option<Price>,
791}
792
793#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
794#[serde(deny_unknown_fields)]
795pub struct CanonicalTarget {
796    pub target_ref: Option<BoundedText>,
797    pub price: Price,
798    #[serde(deserialize_with = "deserialize_positive_quantity")]
799    pub quantity: Quantity,
800}
801
802impl CanonicalTarget {
803    pub fn new(
804        target_ref: Option<BoundedText>,
805        price: Price,
806        quantity: Quantity,
807    ) -> Result<Self, ExecutionEventError> {
808        require_positive_quantity("target", quantity)?;
809        Ok(Self {
810            target_ref,
811            price,
812            quantity,
813        })
814    }
815
816    fn validate(&self) -> Result<(), ExecutionEventError> {
817        require_positive_quantity("target", self.quantity)
818    }
819}
820
821#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
822#[serde(deny_unknown_fields)]
823pub struct CanonicalPositionSnapshot {
824    pub venue_position_ref: VenuePositionRef,
825    #[serde(with = "side_serde")]
826    pub side: Side,
827    pub quantity_before: Quantity,
828    pub quantity_after: Quantity,
829    pub average_open_price: Option<Price>,
830    pub protection: CanonicalProtection,
831    pub targets: Vec<CanonicalTarget>,
832}
833
834#[derive(Deserialize)]
835#[serde(deny_unknown_fields)]
836struct CanonicalPositionSnapshotWire {
837    venue_position_ref: VenuePositionRef,
838    #[serde(with = "side_serde")]
839    side: Side,
840    quantity_before: Quantity,
841    quantity_after: Quantity,
842    average_open_price: Option<Price>,
843    protection: CanonicalProtection,
844    #[serde(deserialize_with = "deserialize_position_targets")]
845    targets: Vec<CanonicalTarget>,
846}
847
848impl CanonicalPositionSnapshot {
849    #[allow(clippy::too_many_arguments)]
850    pub fn new(
851        venue_position_ref: VenuePositionRef,
852        side: Side,
853        quantity_before: Quantity,
854        quantity_after: Quantity,
855        average_open_price: Option<Price>,
856        protection: CanonicalProtection,
857        targets: Vec<CanonicalTarget>,
858    ) -> Result<Self, ExecutionEventError> {
859        let value = Self {
860            venue_position_ref,
861            side,
862            quantity_before,
863            quantity_after,
864            average_open_price,
865            protection,
866            targets,
867        };
868        value.validate()?;
869        Ok(value)
870    }
871
872    fn validate(&self) -> Result<(), ExecutionEventError> {
873        validate_collection_bound("position target", self.targets.len(), MAX_POSITION_TARGETS)?;
874        for target in &self.targets {
875            target.validate()?;
876        }
877        Ok(())
878    }
879}
880
881impl TryFrom<CanonicalPositionSnapshotWire> for CanonicalPositionSnapshot {
882    type Error = ExecutionEventError;
883
884    fn try_from(value: CanonicalPositionSnapshotWire) -> Result<Self, Self::Error> {
885        Self::new(
886            value.venue_position_ref,
887            value.side,
888            value.quantity_before,
889            value.quantity_after,
890            value.average_open_price,
891            value.protection,
892            value.targets,
893        )
894    }
895}
896
897impl<'de> Deserialize<'de> for CanonicalPositionSnapshot {
898    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
899    where
900        D: Deserializer<'de>,
901    {
902        CanonicalPositionSnapshotWire::deserialize(deserializer)?
903            .try_into()
904            .map_err(serde::de::Error::custom)
905    }
906}
907
908#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
909#[serde(deny_unknown_fields)]
910pub struct CanonicalClose {
911    pub venue_position_ref: VenuePositionRef,
912    #[serde(with = "side_serde")]
913    pub side: Side,
914    pub quantity: Quantity,
915    pub price: Price,
916    pub reason: CanonicalCloseReason,
917    pub fees: Vec<FeeAmount>,
918}
919
920#[derive(Deserialize)]
921#[serde(deny_unknown_fields)]
922struct CanonicalCloseWire {
923    venue_position_ref: VenuePositionRef,
924    #[serde(with = "side_serde")]
925    side: Side,
926    #[serde(deserialize_with = "deserialize_positive_quantity")]
927    quantity: Quantity,
928    price: Price,
929    reason: CanonicalCloseReason,
930    #[serde(deserialize_with = "deserialize_close_fees")]
931    fees: Vec<FeeAmount>,
932}
933
934impl CanonicalClose {
935    pub fn new(
936        venue_position_ref: VenuePositionRef,
937        side: Side,
938        quantity: Quantity,
939        price: Price,
940        reason: CanonicalCloseReason,
941        fees: Vec<FeeAmount>,
942    ) -> Result<Self, ExecutionEventError> {
943        let value = Self {
944            venue_position_ref,
945            side,
946            quantity,
947            price,
948            reason,
949            fees,
950        };
951        value.validate()?;
952        Ok(value)
953    }
954
955    fn validate(&self) -> Result<(), ExecutionEventError> {
956        require_positive_quantity("close", self.quantity)?;
957        validate_collection_bound("close fee", self.fees.len(), MAX_CLOSE_FEES)?;
958        for fee in &self.fees {
959            fee.validate()?;
960        }
961        Ok(())
962    }
963}
964
965impl TryFrom<CanonicalCloseWire> for CanonicalClose {
966    type Error = ExecutionEventError;
967
968    fn try_from(value: CanonicalCloseWire) -> Result<Self, Self::Error> {
969        Self::new(
970            value.venue_position_ref,
971            value.side,
972            value.quantity,
973            value.price,
974            value.reason,
975            value.fees,
976        )
977    }
978}
979
980impl<'de> Deserialize<'de> for CanonicalClose {
981    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
982    where
983        D: Deserializer<'de>,
984    {
985        CanonicalCloseWire::deserialize(deserializer)?
986            .try_into()
987            .map_err(serde::de::Error::custom)
988    }
989}
990
991#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
992#[serde(
993    tag = "type",
994    content = "data",
995    rename_all = "snake_case",
996    deny_unknown_fields
997)]
998pub enum ExecutionEvent {
999    VenueAccepted {
1000        order: CanonicalOrderSnapshot,
1001    },
1002    VenueRejected {
1003        category: RejectionCategory,
1004        message: BoundedText,
1005        venue_order_ref: Option<VenueOrderRef>,
1006    },
1007    OrderPartiallyFilled {
1008        fill: CanonicalFill,
1009        order: CanonicalOrderSnapshot,
1010    },
1011    OrderFilled {
1012        fill: CanonicalFill,
1013        order: CanonicalOrderSnapshot,
1014    },
1015    OrderCancelled {
1016        venue_order_ref: VenueOrderRef,
1017        order: Option<CanonicalOrderSnapshot>,
1018        reason: Option<BoundedText>,
1019    },
1020    OrderExpired {
1021        venue_order_ref: VenueOrderRef,
1022        order: Option<CanonicalOrderSnapshot>,
1023    },
1024    ProtectionChanged {
1025        venue_position_ref: VenuePositionRef,
1026        protection: CanonicalProtection,
1027    },
1028    TargetsChanged {
1029        venue_position_ref: VenuePositionRef,
1030        targets: Vec<CanonicalTarget>,
1031    },
1032    PositionChanged {
1033        position: CanonicalPositionSnapshot,
1034        fill: Option<CanonicalFill>,
1035    },
1036    PositionClosed {
1037        close: CanonicalClose,
1038    },
1039    Reconciled {
1040        source: ReconciliationSource,
1041        order: Option<CanonicalOrderSnapshot>,
1042        position: Option<CanonicalPositionSnapshot>,
1043        note: Option<BoundedText>,
1044    },
1045}
1046
1047#[derive(Deserialize)]
1048#[serde(
1049    tag = "type",
1050    content = "data",
1051    rename_all = "snake_case",
1052    deny_unknown_fields
1053)]
1054enum ExecutionEventWire {
1055    VenueAccepted {
1056        order: CanonicalOrderSnapshot,
1057    },
1058    VenueRejected {
1059        category: RejectionCategory,
1060        message: BoundedText,
1061        venue_order_ref: Option<VenueOrderRef>,
1062    },
1063    OrderPartiallyFilled {
1064        fill: CanonicalFill,
1065        order: CanonicalOrderSnapshot,
1066    },
1067    OrderFilled {
1068        fill: CanonicalFill,
1069        order: CanonicalOrderSnapshot,
1070    },
1071    OrderCancelled {
1072        venue_order_ref: VenueOrderRef,
1073        order: Option<CanonicalOrderSnapshot>,
1074        reason: Option<BoundedText>,
1075    },
1076    OrderExpired {
1077        venue_order_ref: VenueOrderRef,
1078        order: Option<CanonicalOrderSnapshot>,
1079    },
1080    ProtectionChanged {
1081        venue_position_ref: VenuePositionRef,
1082        protection: CanonicalProtection,
1083    },
1084    TargetsChanged {
1085        venue_position_ref: VenuePositionRef,
1086        #[serde(deserialize_with = "deserialize_position_targets")]
1087        targets: Vec<CanonicalTarget>,
1088    },
1089    PositionChanged {
1090        position: CanonicalPositionSnapshot,
1091        fill: Option<CanonicalFill>,
1092    },
1093    PositionClosed {
1094        close: CanonicalClose,
1095    },
1096    Reconciled {
1097        source: ReconciliationSource,
1098        order: Option<CanonicalOrderSnapshot>,
1099        position: Option<CanonicalPositionSnapshot>,
1100        note: Option<BoundedText>,
1101    },
1102}
1103
1104impl ExecutionEvent {
1105    pub fn validate(&self) -> Result<(), ExecutionEventError> {
1106        match self {
1107            Self::VenueAccepted { order } => order.validate(),
1108            Self::VenueRejected { .. } | Self::ProtectionChanged { .. } => Ok(()),
1109            Self::OrderPartiallyFilled { fill, order } => {
1110                fill.validate()?;
1111                order.validate()?;
1112                validate_fill_order(fill, order)?;
1113                if fill.remaining_quantity.get().is_zero() {
1114                    return Err(ExecutionEventError::PartialFillWithoutRemainingQuantity);
1115                }
1116                Ok(())
1117            }
1118            Self::OrderFilled { fill, order } => {
1119                fill.validate()?;
1120                order.validate()?;
1121                validate_fill_order(fill, order)?;
1122                if !fill.remaining_quantity.get().is_zero() {
1123                    return Err(ExecutionEventError::FinalFillWithRemainingQuantity);
1124                }
1125                Ok(())
1126            }
1127            Self::OrderCancelled {
1128                venue_order_ref,
1129                order,
1130                ..
1131            }
1132            | Self::OrderExpired {
1133                venue_order_ref,
1134                order,
1135            } => {
1136                if let Some(order) = order {
1137                    order.validate()?;
1138                    if &order.venue_order_ref != venue_order_ref {
1139                        return Err(ExecutionEventError::OrderReferenceMismatch);
1140                    }
1141                }
1142                Ok(())
1143            }
1144            Self::TargetsChanged { targets, .. } => {
1145                validate_collection_bound("position target", targets.len(), MAX_POSITION_TARGETS)?;
1146                for target in targets {
1147                    target.validate()?;
1148                }
1149                Ok(())
1150            }
1151            Self::PositionChanged { position, fill } => {
1152                position.validate()?;
1153                if let Some(fill) = fill {
1154                    fill.validate()?;
1155                }
1156                Ok(())
1157            }
1158            Self::PositionClosed { close } => close.validate(),
1159            Self::Reconciled {
1160                order, position, ..
1161            } => {
1162                if order.is_none() && position.is_none() {
1163                    return Err(ExecutionEventError::EmptyReconciliation);
1164                }
1165                if let Some(order) = order {
1166                    order.validate()?;
1167                }
1168                if let Some(position) = position {
1169                    position.validate()?;
1170                }
1171                Ok(())
1172            }
1173        }
1174    }
1175}
1176
1177impl TryFrom<ExecutionEventWire> for ExecutionEvent {
1178    type Error = ExecutionEventError;
1179
1180    fn try_from(value: ExecutionEventWire) -> Result<Self, Self::Error> {
1181        let event = match value {
1182            ExecutionEventWire::VenueAccepted { order } => Self::VenueAccepted { order },
1183            ExecutionEventWire::VenueRejected {
1184                category,
1185                message,
1186                venue_order_ref,
1187            } => Self::VenueRejected {
1188                category,
1189                message,
1190                venue_order_ref,
1191            },
1192            ExecutionEventWire::OrderPartiallyFilled { fill, order } => {
1193                Self::OrderPartiallyFilled { fill, order }
1194            }
1195            ExecutionEventWire::OrderFilled { fill, order } => Self::OrderFilled { fill, order },
1196            ExecutionEventWire::OrderCancelled {
1197                venue_order_ref,
1198                order,
1199                reason,
1200            } => Self::OrderCancelled {
1201                venue_order_ref,
1202                order,
1203                reason,
1204            },
1205            ExecutionEventWire::OrderExpired {
1206                venue_order_ref,
1207                order,
1208            } => Self::OrderExpired {
1209                venue_order_ref,
1210                order,
1211            },
1212            ExecutionEventWire::ProtectionChanged {
1213                venue_position_ref,
1214                protection,
1215            } => Self::ProtectionChanged {
1216                venue_position_ref,
1217                protection,
1218            },
1219            ExecutionEventWire::TargetsChanged {
1220                venue_position_ref,
1221                targets,
1222            } => Self::TargetsChanged {
1223                venue_position_ref,
1224                targets,
1225            },
1226            ExecutionEventWire::PositionChanged { position, fill } => {
1227                Self::PositionChanged { position, fill }
1228            }
1229            ExecutionEventWire::PositionClosed { close } => Self::PositionClosed { close },
1230            ExecutionEventWire::Reconciled {
1231                source,
1232                order,
1233                position,
1234                note,
1235            } => Self::Reconciled {
1236                source,
1237                order,
1238                position,
1239                note,
1240            },
1241        };
1242        event.validate()?;
1243        Ok(event)
1244    }
1245}
1246
1247impl<'de> Deserialize<'de> for ExecutionEvent {
1248    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1249    where
1250        D: Deserializer<'de>,
1251    {
1252        ExecutionEventWire::deserialize(deserializer)?
1253            .try_into()
1254            .map_err(serde::de::Error::custom)
1255    }
1256}
1257
1258fn validate_fill_order(
1259    fill: &CanonicalFill,
1260    order: &CanonicalOrderSnapshot,
1261) -> Result<(), ExecutionEventError> {
1262    if fill.venue_order_ref != order.venue_order_ref
1263        || fill.side != order.side
1264        || fill.cumulative_quantity != order.cumulative_quantity
1265        || fill.remaining_quantity != order.remaining_quantity
1266    {
1267        return Err(ExecutionEventError::FillOrderMismatch);
1268    }
1269    Ok(())
1270}
1271
1272#[derive(Clone, Debug, Serialize, Deserialize)]
1273#[serde(deny_unknown_fields)]
1274pub struct ExecutionReport {
1275    #[serde(deserialize_with = "deserialize_schema_version")]
1276    pub schema_version: u32,
1277    pub intent_id: Option<TradeIntentId>,
1278    pub command_id: Option<ExecutionCommandId>,
1279    pub execution_venue: ExecutionVenueId,
1280    pub instrument_id: InstrumentId,
1281    pub event_time: DateTimeUtc,
1282    pub received_at: DateTimeUtc,
1283    pub venue_sequence: Option<VenueSequence>,
1284    pub event: ExecutionEvent,
1285    pub opaque_payload_ref: Option<OpaquePayloadRef>,
1286}
1287
1288impl PartialEq for ExecutionReport {
1289    fn eq(&self, other: &Self) -> bool {
1290        self.schema_version == other.schema_version
1291            && self.execution_venue == other.execution_venue
1292            && self.instrument_id == other.instrument_id
1293            && self.event_time == other.event_time
1294            && self.venue_sequence == other.venue_sequence
1295            && self.event == other.event
1296            && self.opaque_payload_ref == other.opaque_payload_ref
1297    }
1298}
1299
1300impl Eq for ExecutionReport {}
1301
1302impl ExecutionReport {
1303    #[allow(clippy::too_many_arguments)]
1304    pub fn new(
1305        intent_id: Option<TradeIntentId>,
1306        command_id: Option<ExecutionCommandId>,
1307        execution_venue: ExecutionVenueId,
1308        instrument_id: InstrumentId,
1309        event_time: DateTimeUtc,
1310        received_at: DateTimeUtc,
1311        venue_sequence: Option<VenueSequence>,
1312        event: ExecutionEvent,
1313        opaque_payload_ref: Option<OpaquePayloadRef>,
1314    ) -> Result<Self, ExecutionEventError> {
1315        event.validate()?;
1316        Ok(Self {
1317            schema_version: EXECUTION_SCHEMA_VERSION,
1318            intent_id,
1319            command_id,
1320            execution_venue,
1321            instrument_id,
1322            event_time,
1323            received_at,
1324            venue_sequence,
1325            event,
1326            opaque_payload_ref,
1327        })
1328    }
1329
1330    pub fn venue_fallback_key(&self) -> VenueEventDedupKey {
1331        match &self.venue_sequence {
1332            Some(venue_sequence) => VenueEventDedupKey::Sequenced {
1333                execution_venue: self.execution_venue.clone(),
1334                instrument_id: self.instrument_id.clone(),
1335                venue_sequence: venue_sequence.clone(),
1336            },
1337            None => VenueEventDedupKey::Unsequenced {
1338                execution_venue: self.execution_venue.clone(),
1339                instrument_id: self.instrument_id.clone(),
1340                event_time: self.event_time,
1341                event: Box::new(self.event.clone()),
1342                opaque_payload_ref: self.opaque_payload_ref.clone(),
1343            },
1344        }
1345    }
1346
1347    pub fn compare(&self, other: &Self) -> ReportComparison {
1348        compare_report_parts(
1349            self.venue_fallback_key() == other.venue_fallback_key(),
1350            self == other,
1351        )
1352    }
1353}
1354
1355#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1356#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1357pub enum VenueEventDedupKey {
1358    Sequenced {
1359        execution_venue: ExecutionVenueId,
1360        instrument_id: InstrumentId,
1361        venue_sequence: VenueSequence,
1362    },
1363    Unsequenced {
1364        execution_venue: ExecutionVenueId,
1365        instrument_id: InstrumentId,
1366        event_time: DateTimeUtc,
1367        event: Box<ExecutionEvent>,
1368        opaque_payload_ref: Option<OpaquePayloadRef>,
1369    },
1370}
1371
1372#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1373pub enum ReportComparison {
1374    Distinct,
1375    Duplicate,
1376    Conflict,
1377}
1378
1379fn compare_report_parts(same_id: bool, same_report: bool) -> ReportComparison {
1380    if !same_id {
1381        ReportComparison::Distinct
1382    } else if same_report {
1383        ReportComparison::Duplicate
1384    } else {
1385        ReportComparison::Conflict
1386    }
1387}
1388
1389#[derive(Clone, Debug, Eq, PartialEq)]
1390pub struct FutureEffectReportContext {
1391    pub report_namespace: ReportNamespace,
1392    pub report_counter: u64,
1393    pub execution_venue: ExecutionVenueId,
1394    pub instrument_id: InstrumentId,
1395    pub event_time: DateTimeUtc,
1396    pub received_at: DateTimeUtc,
1397    pub command_id: Option<ExecutionCommandId>,
1398    pub intent_id: Option<TradeIntentId>,
1399}
1400
1401#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1402pub enum FutureEffectNoReportReason {
1403    InformationalRuleEffect,
1404}
1405
1406#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1407pub enum UnsupportedFutureEffectReason {
1408    NonAuthoritativeOrderPlacement,
1409    MissingAuthoritativeFill,
1410    UnexpectedAuthoritativeFill,
1411}
1412
1413#[derive(Clone, Debug, Eq, PartialEq)]
1414pub enum FutureEffectReportDisposition {
1415    Reports(Vec<ExecutionReport>),
1416    NoReport(FutureEffectNoReportReason),
1417    Unsupported(UnsupportedFutureEffectReason),
1418}
1419
1420pub fn execution_reports_from_future_effect(
1421    effect: &FutureEffect,
1422    context: &FutureEffectReportContext,
1423) -> Result<FutureEffectReportDisposition, ExecutionEventError> {
1424    match effect {
1425        FutureEffect::Plain {
1426            effect,
1427            requested_price,
1428            ..
1429        } => bridge_plain_effect(effect, *requested_price, context),
1430        FutureEffect::Filled { effect, fill, .. } => bridge_filled_effect(
1431            effect,
1432            fill.execution.side,
1433            fill.execution.price,
1434            fill.size,
1435            context,
1436        ),
1437    }
1438}
1439
1440fn bridge_plain_effect(
1441    effect: &Effect,
1442    requested_price: Option<f64>,
1443    context: &FutureEffectReportContext,
1444) -> Result<FutureEffectReportDisposition, ExecutionEventError> {
1445    let event = match effect {
1446        Effect::OrderCancelled { id } => ExecutionEvent::OrderCancelled {
1447            venue_order_ref: venue_order_ref(id)?,
1448            order: None,
1449            reason: None,
1450        },
1451        Effect::StoplossModified { id, new_price, .. } => ExecutionEvent::ProtectionChanged {
1452            venue_position_ref: venue_position_ref(id)?,
1453            protection: CanonicalProtection {
1454                stop_loss: Some(price_from_f64(requested_price.unwrap_or(*new_price))?),
1455            },
1456        },
1457        Effect::StoplossRemoved { id, .. } => ExecutionEvent::ProtectionChanged {
1458            venue_position_ref: venue_position_ref(id)?,
1459            protection: CanonicalProtection::default(),
1460        },
1461        Effect::RuleTriggered { .. } => {
1462            return Ok(FutureEffectReportDisposition::NoReport(
1463                FutureEffectNoReportReason::InformationalRuleEffect,
1464            ));
1465        }
1466        Effect::OrderPlaced { .. } => {
1467            return Ok(FutureEffectReportDisposition::Unsupported(
1468                UnsupportedFutureEffectReason::NonAuthoritativeOrderPlacement,
1469            ));
1470        }
1471        Effect::PositionOpened { .. }
1472        | Effect::PositionClosed { .. }
1473        | Effect::PartialClose { .. }
1474        | Effect::ScaledIn { .. } => {
1475            return Ok(FutureEffectReportDisposition::Unsupported(
1476                UnsupportedFutureEffectReason::MissingAuthoritativeFill,
1477            ));
1478        }
1479    };
1480    Ok(FutureEffectReportDisposition::Reports(vec![bridge_report(
1481        context, 0, event,
1482    )?]))
1483}
1484
1485fn bridge_filled_effect(
1486    effect: &Effect,
1487    side: Side,
1488    fill_price: f64,
1489    fill_size: f64,
1490    context: &FutureEffectReportContext,
1491) -> Result<FutureEffectReportDisposition, ExecutionEventError> {
1492    let price = price_from_f64(fill_price)?;
1493    let quantity = positive_quantity_from_f64("future fill", fill_size)?;
1494    match effect {
1495        Effect::PositionOpened { id } => {
1496            let order_ref = venue_order_ref(id)?;
1497            let fill = completed_fill(order_ref.clone(), side, price, quantity)?;
1498            let order = completed_order(order_ref, side, price, quantity)?;
1499            let position = CanonicalPositionSnapshot::new(
1500                venue_position_ref(id)?,
1501                side,
1502                Quantity::new(Decimal::ZERO)
1503                    .map_err(|error| ExecutionEventError::InvalidDecimal(error.to_string()))?,
1504                quantity,
1505                Some(price),
1506                CanonicalProtection::default(),
1507                Vec::new(),
1508            )?;
1509            Ok(FutureEffectReportDisposition::Reports(vec![
1510                bridge_report(context, 0, ExecutionEvent::OrderFilled { fill, order })?,
1511                bridge_report(
1512                    context,
1513                    1,
1514                    ExecutionEvent::PositionChanged {
1515                        position,
1516                        fill: None,
1517                    },
1518                )?,
1519            ]))
1520        }
1521        Effect::ScaledIn { id, .. } => {
1522            let order_ref = venue_order_ref(id)?;
1523            let fill = completed_fill(order_ref.clone(), side, price, quantity)?;
1524            let order = completed_order(order_ref, side, price, quantity)?;
1525            Ok(FutureEffectReportDisposition::Reports(vec![bridge_report(
1526                context,
1527                0,
1528                ExecutionEvent::OrderFilled { fill, order },
1529            )?]))
1530        }
1531        Effect::PositionClosed { id, reason } => {
1532            let close = CanonicalClose::new(
1533                venue_position_ref(id)?,
1534                side,
1535                quantity,
1536                price,
1537                canonical_close_reason(*reason),
1538                Vec::new(),
1539            )?;
1540            Ok(FutureEffectReportDisposition::Reports(vec![bridge_report(
1541                context,
1542                0,
1543                ExecutionEvent::PositionClosed { close },
1544            )?]))
1545        }
1546        Effect::PartialClose { id, ratio, .. } => {
1547            if !ratio.is_finite() || *ratio <= 0.0 || *ratio >= 1.0 {
1548                return Err(ExecutionEventError::InvalidPartialCloseRatio);
1549            }
1550            let quantity_before =
1551                positive_quantity_from_f64("position before partial close", fill_size / ratio)?;
1552            let quantity_after = quantity_from_f64(fill_size * (1.0 - ratio) / ratio)?;
1553            let position = CanonicalPositionSnapshot::new(
1554                venue_position_ref(id)?,
1555                side,
1556                quantity_before,
1557                quantity_after,
1558                None,
1559                CanonicalProtection::default(),
1560                Vec::new(),
1561            )?;
1562            let order_ref = venue_order_ref(id)?;
1563            let fill = completed_fill(order_ref, side, price, quantity)?;
1564            Ok(FutureEffectReportDisposition::Reports(vec![bridge_report(
1565                context,
1566                0,
1567                ExecutionEvent::PositionChanged {
1568                    position,
1569                    fill: Some(fill),
1570                },
1571            )?]))
1572        }
1573        Effect::OrderPlaced { .. }
1574        | Effect::OrderCancelled { .. }
1575        | Effect::StoplossModified { .. }
1576        | Effect::StoplossRemoved { .. }
1577        | Effect::RuleTriggered { .. } => Ok(FutureEffectReportDisposition::Unsupported(
1578            UnsupportedFutureEffectReason::UnexpectedAuthoritativeFill,
1579        )),
1580    }
1581}
1582
1583fn bridge_report(
1584    context: &FutureEffectReportContext,
1585    offset: u64,
1586    event: ExecutionEvent,
1587) -> Result<ExecutionReport, ExecutionEventError> {
1588    let counter = context
1589        .report_counter
1590        .checked_add(offset)
1591        .ok_or(ExecutionEventError::ReportCounterOverflow)?;
1592    let venue_sequence =
1593        VenueSequence::new(format!("{}:{counter}", context.report_namespace.as_str()))?;
1594    ExecutionReport::new(
1595        context.intent_id.clone(),
1596        context.command_id.clone(),
1597        context.execution_venue.clone(),
1598        context.instrument_id.clone(),
1599        context.event_time,
1600        context.received_at,
1601        Some(venue_sequence),
1602        event,
1603        None,
1604    )
1605}
1606
1607fn completed_fill(
1608    venue_order_ref: VenueOrderRef,
1609    side: Side,
1610    price: Price,
1611    quantity: Quantity,
1612) -> Result<CanonicalFill, ExecutionEventError> {
1613    CanonicalFill::new(
1614        None,
1615        venue_order_ref,
1616        side,
1617        price,
1618        quantity,
1619        quantity,
1620        quantity_from_decimal(Decimal::ZERO)?,
1621        LiquidityRole::Unknown,
1622        Vec::new(),
1623    )
1624}
1625
1626fn completed_order(
1627    venue_order_ref: VenueOrderRef,
1628    side: Side,
1629    price: Price,
1630    quantity: Quantity,
1631) -> Result<CanonicalOrderSnapshot, ExecutionEventError> {
1632    CanonicalOrderSnapshot::new(
1633        venue_order_ref,
1634        side,
1635        quantity,
1636        quantity,
1637        quantity_from_decimal(Decimal::ZERO)?,
1638        Some(price),
1639    )
1640}
1641
1642fn quantity_from_decimal(value: Decimal) -> Result<Quantity, ExecutionEventError> {
1643    Quantity::new(value).map_err(|error| ExecutionEventError::InvalidDecimal(error.to_string()))
1644}
1645
1646fn quantity_from_f64(value: f64) -> Result<Quantity, ExecutionEventError> {
1647    let decimal = Decimal::checked_from_f64(value)
1648        .map_err(|error| ExecutionEventError::InvalidDecimal(error.to_string()))?;
1649    quantity_from_decimal(decimal)
1650}
1651
1652fn positive_quantity_from_f64(
1653    kind: &'static str,
1654    value: f64,
1655) -> Result<Quantity, ExecutionEventError> {
1656    let quantity = quantity_from_f64(value)?;
1657    require_positive_quantity(kind, quantity)?;
1658    Ok(quantity)
1659}
1660
1661fn price_from_f64(value: f64) -> Result<Price, ExecutionEventError> {
1662    let decimal = Decimal::checked_from_f64(value)
1663        .map_err(|error| ExecutionEventError::InvalidDecimal(error.to_string()))?;
1664    Price::new(decimal).map_err(|error| ExecutionEventError::InvalidDecimal(error.to_string()))
1665}
1666
1667fn venue_order_ref(value: &str) -> Result<VenueOrderRef, ExecutionEventError> {
1668    VenueOrderRef::new(value)
1669        .map_err(|error| ExecutionEventError::InvalidIdentity(error.to_string()))
1670}
1671
1672fn venue_position_ref(value: &str) -> Result<VenuePositionRef, ExecutionEventError> {
1673    VenuePositionRef::new(value)
1674        .map_err(|error| ExecutionEventError::InvalidIdentity(error.to_string()))
1675}
1676
1677fn canonical_close_reason(reason: CloseReason) -> CanonicalCloseReason {
1678    match reason {
1679        CloseReason::Stoploss => CanonicalCloseReason::StopLoss,
1680        CloseReason::Target => CanonicalCloseReason::Target,
1681        CloseReason::TrailingStop => CanonicalCloseReason::TrailingStop,
1682        CloseReason::TimeExit => CanonicalCloseReason::TimeExit,
1683        CloseReason::BreakevenStop => CanonicalCloseReason::BreakevenStop,
1684        CloseReason::Manual => CanonicalCloseReason::Manual,
1685        CloseReason::EndOfData => CanonicalCloseReason::EndOfData,
1686        CloseReason::GroupRule => CanonicalCloseReason::GroupRule,
1687        CloseReason::Cancelled => CanonicalCloseReason::Cancelled,
1688    }
1689}