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#[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}