Skip to main content

nautilus_betfair/common/
enums.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Common enumerations for the Betfair adapter.
17
18use nautilus_model::enums::{
19    MarketStatus as NautilusMarketStatus, OrderSide, OrderStatus, OrderType, TimeInForce,
20};
21use rust_decimal::Decimal;
22use serde::{Deserialize, Serialize};
23use strum::{AsRefStr, Display, EnumIter, EnumString};
24
25/// Betfair order side.
26#[derive(
27    Clone,
28    Copy,
29    Debug,
30    PartialEq,
31    Eq,
32    Hash,
33    AsRefStr,
34    Display,
35    EnumIter,
36    EnumString,
37    Serialize,
38    Deserialize,
39)]
40#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
41#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
42pub enum BetfairSide {
43    /// Betting on the selection to win.
44    Back,
45    /// Betting against the selection to win.
46    Lay,
47}
48
49/// Betfair order type.
50#[derive(
51    Clone,
52    Copy,
53    Debug,
54    PartialEq,
55    Eq,
56    Hash,
57    AsRefStr,
58    Display,
59    EnumIter,
60    EnumString,
61    Serialize,
62    Deserialize,
63)]
64#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
65#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
66pub enum BetfairOrderType {
67    /// A normal exchange limit order for immediate execution.
68    Limit,
69    /// Limit order for the auction (SP).
70    LimitOnClose,
71    /// Market order for the auction (SP).
72    MarketOnClose,
73    /// Legacy name for `MarketOnClose` (appears in older settled orders).
74    MarketAtTheClose,
75}
76
77/// Betfair order status.
78#[derive(
79    Clone,
80    Copy,
81    Debug,
82    PartialEq,
83    Eq,
84    Hash,
85    AsRefStr,
86    Display,
87    EnumIter,
88    EnumString,
89    Serialize,
90    Deserialize,
91)]
92#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
93#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
94pub enum BetfairOrderStatus {
95    /// Order is pending.
96    Pending,
97    /// Order has been fully matched/cancelled/lapsed.
98    ExecutionComplete,
99    /// Order has remaining unmatched volume.
100    Executable,
101    /// Order has expired.
102    Expired,
103}
104
105/// Controls which data fields are returned with market catalogues.
106#[derive(
107    Clone,
108    Copy,
109    Debug,
110    PartialEq,
111    Eq,
112    Hash,
113    AsRefStr,
114    Display,
115    EnumIter,
116    EnumString,
117    Serialize,
118    Deserialize,
119)]
120#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
121#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
122pub enum MarketProjection {
123    Competition,
124    Event,
125    EventType,
126    MarketStartTime,
127    MarketDescription,
128    RunnerDescription,
129    RunnerMetadata,
130}
131
132/// Market status.
133#[derive(
134    Clone,
135    Copy,
136    Debug,
137    PartialEq,
138    Eq,
139    Hash,
140    AsRefStr,
141    Display,
142    EnumIter,
143    EnumString,
144    Serialize,
145    Deserialize,
146)]
147#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
148#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
149pub enum MarketStatus {
150    Inactive,
151    Open,
152    Suspended,
153    Closed,
154    #[serde(other)]
155    Unknown,
156}
157
158/// Sorting options for market listings.
159#[derive(
160    Clone,
161    Copy,
162    Debug,
163    PartialEq,
164    Eq,
165    Hash,
166    AsRefStr,
167    Display,
168    EnumIter,
169    EnumString,
170    Serialize,
171    Deserialize,
172)]
173#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
174#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
175pub enum MarketSort {
176    MinimumTraded,
177    MaximumTraded,
178    MinimumAvailable,
179    MaximumAvailable,
180    FirstToStart,
181    LastToStart,
182}
183
184/// Market betting type.
185#[derive(
186    Clone,
187    Copy,
188    Debug,
189    PartialEq,
190    Eq,
191    Hash,
192    AsRefStr,
193    Display,
194    EnumIter,
195    EnumString,
196    Serialize,
197    Deserialize,
198)]
199#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
200#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
201pub enum MarketBettingType {
202    Odds,
203    Line,
204    Range,
205    AsianHandicapDoubleLine,
206    AsianHandicapSingleLine,
207    FixedOdds,
208    #[serde(other)]
209    Unknown,
210}
211
212/// Exchange price data options.
213#[derive(
214    Clone,
215    Copy,
216    Debug,
217    PartialEq,
218    Eq,
219    Hash,
220    AsRefStr,
221    Display,
222    EnumIter,
223    EnumString,
224    Serialize,
225    Deserialize,
226)]
227#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
228#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
229pub enum PriceData {
230    SpAvailable,
231    SpTraded,
232    ExBestOffers,
233    ExAllOffers,
234    ExTraded,
235}
236
237/// Matched amount rollup projection.
238#[derive(
239    Clone,
240    Copy,
241    Debug,
242    PartialEq,
243    Eq,
244    Hash,
245    AsRefStr,
246    Display,
247    EnumIter,
248    EnumString,
249    Serialize,
250    Deserialize,
251)]
252#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
253#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
254pub enum MatchProjection {
255    NoRollup,
256    RolledUpByPrice,
257    RolledUpByAvgPrice,
258}
259
260/// Price ladder type.
261#[derive(
262    Clone,
263    Copy,
264    Debug,
265    PartialEq,
266    Eq,
267    Hash,
268    AsRefStr,
269    Display,
270    EnumIter,
271    EnumString,
272    Serialize,
273    Deserialize,
274)]
275#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
276#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
277pub enum PriceLadderType {
278    Classic,
279    Finest,
280    LineRange,
281    #[serde(other)]
282    Unknown,
283}
284
285/// Order filter projection.
286#[derive(
287    Clone,
288    Copy,
289    Debug,
290    PartialEq,
291    Eq,
292    Hash,
293    AsRefStr,
294    Display,
295    EnumIter,
296    EnumString,
297    Serialize,
298    Deserialize,
299)]
300#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
301#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
302pub enum OrderProjection {
303    All,
304    Executable,
305    ExecutionComplete,
306}
307
308/// Order sort field.
309#[derive(
310    Clone,
311    Copy,
312    Debug,
313    PartialEq,
314    Eq,
315    Hash,
316    AsRefStr,
317    Display,
318    EnumIter,
319    EnumString,
320    Serialize,
321    Deserialize,
322)]
323#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
324#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
325pub enum OrderBy {
326    ByBet,
327    ByMarket,
328    ByMatchTime,
329    ByPlaceTime,
330    BySettledTime,
331    ByVoidTime,
332}
333
334/// Sort direction for order listings.
335#[derive(
336    Clone,
337    Copy,
338    Debug,
339    PartialEq,
340    Eq,
341    Hash,
342    AsRefStr,
343    Display,
344    EnumIter,
345    EnumString,
346    Serialize,
347    Deserialize,
348)]
349#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
350#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
351pub enum SortDir {
352    EarliestToLatest,
353    LatestToEarliest,
354}
355
356/// Betfair time-in-force (only FILL_OR_KILL supported).
357#[derive(
358    Clone,
359    Copy,
360    Debug,
361    PartialEq,
362    Eq,
363    Hash,
364    AsRefStr,
365    Display,
366    EnumIter,
367    EnumString,
368    Serialize,
369    Deserialize,
370)]
371#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
372#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
373pub enum BetfairTimeInForce {
374    FillOrKill,
375}
376
377/// How unmatched bets are handled at market turn in-play.
378#[derive(
379    Clone,
380    Copy,
381    Debug,
382    PartialEq,
383    Eq,
384    Hash,
385    AsRefStr,
386    Display,
387    EnumIter,
388    EnumString,
389    Serialize,
390    Deserialize,
391)]
392#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
393#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
394pub enum PersistenceType {
395    /// Bet is lapsed (cancelled) when market turns in-play.
396    Lapse,
397    /// Bet persists when market turns in-play.
398    Persist,
399    /// Bet is placed as a Market On Close order.
400    MarketOnClose,
401}
402
403/// Execution report status for batch order operations.
404#[derive(
405    Clone,
406    Copy,
407    Debug,
408    PartialEq,
409    Eq,
410    Hash,
411    AsRefStr,
412    Display,
413    EnumIter,
414    EnumString,
415    Serialize,
416    Deserialize,
417)]
418#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
419#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
420pub enum ExecutionReportStatus {
421    Success,
422    Failure,
423    ProcessedWithErrors,
424    Timeout,
425}
426
427/// Error codes for execution report failures.
428#[derive(
429    Clone,
430    Copy,
431    Debug,
432    PartialEq,
433    Eq,
434    Hash,
435    AsRefStr,
436    Display,
437    EnumIter,
438    EnumString,
439    Serialize,
440    Deserialize,
441)]
442#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
443#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
444pub enum ExecutionReportErrorCode {
445    ErrorInMatcher,
446    ProcessedWithErrors,
447    BetActionError,
448    InvalidAccountState,
449    InvalidWalletStatus,
450    InsufficientFunds,
451    LossLimitExceeded,
452    MarketSuspended,
453    MarketNotOpenForBetting,
454    DuplicateTransaction,
455    InvalidOrder,
456    InvalidMarketId,
457    PermissionDenied,
458    DuplicateBetids,
459    NoActionRequired,
460    ServiceUnavailable,
461    RejectedByRegulator,
462    NoChasing,
463    RegulatorIsNotAvailable,
464    TooManyInstructions,
465    InvalidMarketVersion,
466    InvalidProfitRatio,
467    EventExposureLimitExceeded,
468    EventMatchedExposureLimitExceeded,
469    EventBlocked,
470}
471
472/// Instruction report status for individual order instructions.
473#[derive(
474    Clone,
475    Copy,
476    Debug,
477    PartialEq,
478    Eq,
479    Hash,
480    AsRefStr,
481    Display,
482    EnumIter,
483    EnumString,
484    Serialize,
485    Deserialize,
486)]
487#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
488#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
489pub enum InstructionReportStatus {
490    Success,
491    Failure,
492    Timeout,
493}
494
495/// Error codes for individual instruction report failures.
496#[derive(
497    Clone,
498    Copy,
499    Debug,
500    PartialEq,
501    Eq,
502    Hash,
503    AsRefStr,
504    Display,
505    EnumIter,
506    EnumString,
507    Serialize,
508    Deserialize,
509)]
510#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
511#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
512pub enum InstructionReportErrorCode {
513    InvalidBetSize,
514    InvalidRunner,
515    BetTakenOrLapsed,
516    BetInProgress,
517    RunnerRemoved,
518    MarketNotOpenForBetting,
519    LossLimitExceeded,
520    MarketNotOpenForBspBetting,
521    InvalidPriceEdit,
522    InvalidOdds,
523    InsufficientFunds,
524    InvalidPersistenceType,
525    ErrorInMatcher,
526    InvalidBackLayCombination,
527    ErrorInOrder,
528    InvalidBidType,
529    InvalidBetId,
530    CancelledNotPlaced,
531    RelatedActionFailed,
532    NoActionRequired,
533    TimeInForceConflict,
534    UnexpectedPersistenceType,
535    InvalidOrderType,
536    UnexpectedMinFillSize,
537    InvalidCustomerOrderRef,
538    InvalidMinFillSize,
539    BetLapsedPriceImprovementTooLarge,
540    InvalidCustomerStrategyRef,
541    InvalidProfitRatio,
542}
543
544/// Runner status.
545#[derive(
546    Clone,
547    Copy,
548    Debug,
549    PartialEq,
550    Eq,
551    Hash,
552    AsRefStr,
553    Display,
554    EnumIter,
555    EnumString,
556    Serialize,
557    Deserialize,
558)]
559#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
560#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
561pub enum RunnerStatus {
562    Active,
563    Winner,
564    Loser,
565    Placed,
566    RemovedVacant,
567    Removed,
568    Hidden,
569    #[serde(other)]
570    Unknown,
571}
572
573/// Bet settlement status.
574#[derive(
575    Clone,
576    Copy,
577    Debug,
578    PartialEq,
579    Eq,
580    Hash,
581    AsRefStr,
582    Display,
583    EnumIter,
584    EnumString,
585    Serialize,
586    Deserialize,
587)]
588#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
589#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
590pub enum BetStatus {
591    Settled,
592    Voided,
593    Lapsed,
594    Cancelled,
595}
596
597/// Grouping level for cleared order reports.
598#[derive(
599    Clone,
600    Copy,
601    Debug,
602    PartialEq,
603    Eq,
604    Hash,
605    AsRefStr,
606    Display,
607    EnumIter,
608    EnumString,
609    Serialize,
610    Deserialize,
611)]
612#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
613#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
614pub enum GroupBy {
615    EventType,
616    Event,
617    Market,
618    Side,
619    Bet,
620    Runner,
621    Strategy,
622}
623
624/// Time aggregation granularity.
625#[derive(
626    Clone,
627    Copy,
628    Debug,
629    PartialEq,
630    Eq,
631    Hash,
632    AsRefStr,
633    Display,
634    EnumIter,
635    EnumString,
636    Serialize,
637    Deserialize,
638)]
639#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
640#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
641pub enum TimeGranularity {
642    Days,
643    Hours,
644    Minutes,
645}
646
647/// Bet target type.
648#[derive(
649    Clone,
650    Copy,
651    Debug,
652    PartialEq,
653    Eq,
654    Hash,
655    AsRefStr,
656    Display,
657    EnumIter,
658    EnumString,
659    Serialize,
660    Deserialize,
661)]
662#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
663#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
664pub enum BetTargetType {
665    BackersProfit,
666    Payout,
667}
668
669/// Bet delay model for in-play markets.
670#[derive(
671    Clone,
672    Copy,
673    Debug,
674    PartialEq,
675    Eq,
676    Hash,
677    AsRefStr,
678    Display,
679    EnumIter,
680    EnumString,
681    Serialize,
682    Deserialize,
683)]
684#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
685#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
686pub enum BetDelayModel {
687    Passive,
688    Dynamic,
689}
690
691/// Volume rollup strategy.
692#[derive(
693    Clone,
694    Copy,
695    Debug,
696    PartialEq,
697    Eq,
698    Hash,
699    AsRefStr,
700    Display,
701    EnumIter,
702    EnumString,
703    Serialize,
704    Deserialize,
705)]
706#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
707#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
708pub enum RollupModel {
709    Stake,
710    Payout,
711    ManagedLiability,
712    None,
713}
714
715/// Certificate-based login response status.
716#[derive(
717    Clone,
718    Copy,
719    Debug,
720    PartialEq,
721    Eq,
722    Hash,
723    AsRefStr,
724    Display,
725    EnumIter,
726    EnumString,
727    Serialize,
728    Deserialize,
729)]
730#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
731#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
732pub enum CertLoginStatus {
733    Success,
734    NoError,
735    Fail,
736    AccountAlreadyLocked,
737    AccountNowLocked,
738    AccountPendingPasswordChange,
739    ActionsRequired,
740    AgentClientMaster,
741    AgentClientMasterSuspended,
742    AuthorizedOnlyForDomainRo,
743    AuthorizedOnlyForDomainSe,
744    BettingRestrictedLocation,
745    CertAuthRequired,
746    ChangePasswordRequired,
747    Closed,
748    DanishAuthorizationRequired,
749    DenmarkMigrationRequired,
750    DuplicateCards,
751    EmailLoginNotAllowed,
752    InputValidationError,
753    InternalError,
754    InternationalTermsAcceptanceRequired,
755    InvalidConnectivityToRegulatorDk,
756    InvalidConnectivityToRegulatorIt,
757    InvalidUsernameOrPassword,
758    ItalianContractAcceptanceRequired,
759    ItalianProfilingAcceptanceRequired,
760    KycSuspend,
761    LoginRestricted,
762    MultipleUsersWithSameCredential,
763    NotAuthorizedByRegulatorDk,
764    NotAuthorizedByRegulatorIt,
765    PendingAuth,
766    PersonalMessageRequired,
767    #[serde(rename = "SECURITY_QUESTION_WRONG_3X")]
768    #[strum(serialize = "SECURITY_QUESTION_WRONG_3X")]
769    SecurityQuestionWrong3x,
770    SecurityRestrictedLocation,
771    SelfExcluded,
772    SpainMigrationRequired,
773    SpanishTermsAcceptanceRequired,
774    StrongAuthCodeRequired,
775    Suspended,
776    SwedenBankIdVerificationRequired,
777    SwedenNationalIdentifierRequired,
778    TelbetTermsConditionsNa,
779    TemporaryBanTooManyRequests,
780    TradingMaster,
781    TradingMasterSuspended,
782    #[serde(other)]
783    Other,
784}
785
786/// Streaming order side (shorthand: B=Back, L=Lay).
787#[derive(
788    Clone,
789    Copy,
790    Debug,
791    PartialEq,
792    Eq,
793    Hash,
794    AsRefStr,
795    Display,
796    EnumIter,
797    EnumString,
798    Serialize,
799    Deserialize,
800)]
801pub enum StreamingSide {
802    #[serde(rename = "B")]
803    #[strum(serialize = "B")]
804    Back,
805    #[serde(rename = "L")]
806    #[strum(serialize = "L")]
807    Lay,
808}
809
810/// Streaming order status (shorthand: E=Executable, EC=ExecutionComplete).
811#[derive(
812    Clone,
813    Copy,
814    Debug,
815    PartialEq,
816    Eq,
817    Hash,
818    AsRefStr,
819    Display,
820    EnumIter,
821    EnumString,
822    Serialize,
823    Deserialize,
824)]
825pub enum StreamingOrderStatus {
826    #[serde(rename = "E")]
827    #[strum(serialize = "E")]
828    Executable,
829    #[serde(rename = "EC")]
830    #[strum(serialize = "EC")]
831    ExecutionComplete,
832}
833
834/// Streaming persistence type (shorthand: L=Lapse, P=Persist, MOC=MarketOnClose).
835#[derive(
836    Clone,
837    Copy,
838    Debug,
839    PartialEq,
840    Eq,
841    Hash,
842    AsRefStr,
843    Display,
844    EnumIter,
845    EnumString,
846    Serialize,
847    Deserialize,
848)]
849pub enum StreamingPersistenceType {
850    #[serde(rename = "L")]
851    #[strum(serialize = "L")]
852    Lapse,
853    #[serde(rename = "P")]
854    #[strum(serialize = "P")]
855    Persist,
856    #[serde(rename = "MOC")]
857    #[strum(serialize = "MOC")]
858    MarketOnClose,
859}
860
861/// Streaming order type (shorthand: L=Limit, LOC=LimitOnClose, MOC=MarketOnClose).
862#[derive(
863    Clone,
864    Copy,
865    Debug,
866    PartialEq,
867    Eq,
868    Hash,
869    AsRefStr,
870    Display,
871    EnumIter,
872    EnumString,
873    Serialize,
874    Deserialize,
875)]
876pub enum StreamingOrderType {
877    #[serde(rename = "L")]
878    #[strum(serialize = "L")]
879    Limit,
880    #[serde(rename = "LOC")]
881    #[strum(serialize = "LOC")]
882    LimitOnClose,
883    #[serde(rename = "MOC")]
884    #[strum(serialize = "MOC")]
885    MarketOnClose,
886}
887
888/// Streaming status error code.
889#[derive(
890    Clone,
891    Copy,
892    Debug,
893    PartialEq,
894    Eq,
895    Hash,
896    AsRefStr,
897    Display,
898    EnumIter,
899    EnumString,
900    Serialize,
901    Deserialize,
902)]
903#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
904#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
905pub enum StatusErrorCode {
906    InvalidInput,
907    Timeout,
908    NoAppKey,
909    InvalidAppKey,
910    NoSession,
911    InvalidSessionInformation,
912    NotAuthorized,
913    MaxConnectionLimitExceeded,
914    TooManyRequests,
915    SubscriptionLimitExceeded,
916    InvalidClock,
917    UnexpectedError,
918    ConnectionFailed,
919    InvalidRequest,
920}
921
922impl StatusErrorCode {
923    /// Returns `true` for errors that will never succeed on retry and should
924    /// permanently disable the race stream.
925    #[must_use]
926    pub fn is_race_stream_fatal(&self) -> bool {
927        matches!(
928            self,
929            Self::NoAppKey
930                | Self::InvalidAppKey
931                | Self::NotAuthorized
932                | Self::SubscriptionLimitExceeded
933                | Self::MaxConnectionLimitExceeded
934        )
935    }
936}
937
938/// Streaming change type.
939#[derive(
940    Clone,
941    Copy,
942    Debug,
943    PartialEq,
944    Eq,
945    Hash,
946    AsRefStr,
947    Display,
948    EnumIter,
949    EnumString,
950    Serialize,
951    Deserialize,
952)]
953#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
954#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
955pub enum ChangeType {
956    Heartbeat,
957    SubImage,
958    ResubDelta,
959}
960
961/// Streaming segment type.
962#[derive(
963    Clone,
964    Copy,
965    Debug,
966    PartialEq,
967    Eq,
968    Hash,
969    AsRefStr,
970    Display,
971    EnumIter,
972    EnumString,
973    Serialize,
974    Deserialize,
975)]
976#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
977#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
978pub enum SegmentType {
979    SegStart,
980    Seg,
981    SegEnd,
982}
983
984/// Reason code for bet lapse events on the streaming API.
985#[derive(
986    Clone,
987    Copy,
988    Debug,
989    PartialEq,
990    Eq,
991    Hash,
992    AsRefStr,
993    Display,
994    EnumIter,
995    EnumString,
996    Serialize,
997    Deserialize,
998)]
999#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1000#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1001pub enum LapseStatusReasonCode {
1002    MktUnknown,
1003    MktInvalid,
1004    RnrUnknown,
1005    TimeElapsed,
1006    CurrencyUnknown,
1007    PriceInvalid,
1008    MktSuspended,
1009    MktVersion,
1010    LineTarget,
1011    LineSp,
1012    SpInPlay,
1013    SmallStake,
1014    PriceImpTooLarge,
1015}
1016
1017/// Market data filter fields for streaming subscriptions.
1018#[derive(
1019    Clone,
1020    Copy,
1021    Debug,
1022    PartialEq,
1023    Eq,
1024    Hash,
1025    AsRefStr,
1026    Display,
1027    EnumIter,
1028    EnumString,
1029    Serialize,
1030    Deserialize,
1031)]
1032#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1033#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1034pub enum MarketDataFilterField {
1035    ExBestOffersDisp,
1036    ExBestOffers,
1037    ExAllOffers,
1038    ExTraded,
1039    ExTradedVol,
1040    ExLtp,
1041    ExMarketDef,
1042    SpTraded,
1043    SpProjected,
1044}
1045
1046// Betfair side mapping is INVERTED from financial convention:
1047// Back (betting on selection to win) = SELL
1048// Lay (betting against selection) = BUY
1049
1050impl From<BetfairSide> for OrderSide {
1051    fn from(value: BetfairSide) -> Self {
1052        match value {
1053            BetfairSide::Back => Self::Sell,
1054            BetfairSide::Lay => Self::Buy,
1055        }
1056    }
1057}
1058
1059impl From<OrderSide> for BetfairSide {
1060    fn from(value: OrderSide) -> Self {
1061        match value {
1062            OrderSide::Buy => Self::Lay,
1063            OrderSide::Sell => Self::Back,
1064            _ => panic!("Invalid `OrderSide` for Betfair: {value}"),
1065        }
1066    }
1067}
1068
1069impl From<StreamingSide> for OrderSide {
1070    fn from(value: StreamingSide) -> Self {
1071        match value {
1072            StreamingSide::Back => Self::Sell,
1073            StreamingSide::Lay => Self::Buy,
1074        }
1075    }
1076}
1077
1078impl From<BetfairOrderType> for OrderType {
1079    fn from(value: BetfairOrderType) -> Self {
1080        match value {
1081            BetfairOrderType::Limit => Self::Limit,
1082            BetfairOrderType::LimitOnClose => Self::Limit,
1083            BetfairOrderType::MarketOnClose => Self::Market,
1084            BetfairOrderType::MarketAtTheClose => Self::Market,
1085        }
1086    }
1087}
1088
1089impl From<StreamingOrderType> for OrderType {
1090    fn from(value: StreamingOrderType) -> Self {
1091        match value {
1092            StreamingOrderType::Limit => Self::Limit,
1093            StreamingOrderType::LimitOnClose => Self::Limit,
1094            StreamingOrderType::MarketOnClose => Self::Market,
1095        }
1096    }
1097}
1098
1099/// Resolves the Nautilus `OrderStatus` for a Betfair order.
1100///
1101/// `ExecutionComplete` is a terminal state covering fills, cancels, and
1102/// lapses — the correct status depends on matched vs canceled quantities.
1103#[must_use]
1104pub fn resolve_order_status(
1105    status: BetfairOrderStatus,
1106    size_matched: Decimal,
1107    size_cancelled: Decimal,
1108) -> OrderStatus {
1109    match status {
1110        BetfairOrderStatus::Pending => OrderStatus::Submitted,
1111        BetfairOrderStatus::Executable if size_matched > Decimal::ZERO => {
1112            OrderStatus::PartiallyFilled
1113        }
1114        BetfairOrderStatus::Executable => OrderStatus::Accepted,
1115        BetfairOrderStatus::Expired => OrderStatus::Expired,
1116        BetfairOrderStatus::ExecutionComplete => {
1117            resolve_terminal_status(size_matched, size_cancelled)
1118        }
1119    }
1120}
1121
1122/// Resolves the Nautilus `OrderStatus` for a streaming order update.
1123///
1124/// Same logic as [`resolve_order_status`] for the streaming enum.
1125#[must_use]
1126pub fn resolve_streaming_order_status(
1127    status: StreamingOrderStatus,
1128    size_matched: Decimal,
1129    size_cancelled: Decimal,
1130) -> OrderStatus {
1131    match status {
1132        StreamingOrderStatus::Executable if size_matched > Decimal::ZERO => {
1133            OrderStatus::PartiallyFilled
1134        }
1135        StreamingOrderStatus::Executable => OrderStatus::Accepted,
1136        StreamingOrderStatus::ExecutionComplete => {
1137            resolve_terminal_status(size_matched, size_cancelled)
1138        }
1139    }
1140}
1141
1142fn resolve_terminal_status(size_matched: Decimal, size_cancelled: Decimal) -> OrderStatus {
1143    if size_matched > Decimal::ZERO && size_cancelled <= Decimal::ZERO {
1144        OrderStatus::Filled
1145    } else {
1146        // Any terminal order with cancelled quantity is closed, even if
1147        // partially matched. PartiallyFilled is an open status in Nautilus
1148        // and must not be used for ExecutionComplete orders.
1149        OrderStatus::Canceled
1150    }
1151}
1152
1153impl From<MarketStatus> for NautilusMarketStatus {
1154    fn from(value: MarketStatus) -> Self {
1155        match value {
1156            MarketStatus::Open => Self::Open,
1157            MarketStatus::Closed => Self::Closed,
1158            MarketStatus::Suspended => Self::Suspended,
1159            MarketStatus::Inactive | MarketStatus::Unknown => Self::NotAvailable,
1160        }
1161    }
1162}
1163
1164impl From<BetfairTimeInForce> for TimeInForce {
1165    fn from(value: BetfairTimeInForce) -> Self {
1166        match value {
1167            BetfairTimeInForce::FillOrKill => Self::Fok,
1168        }
1169    }
1170}
1171
1172impl From<PersistenceType> for TimeInForce {
1173    fn from(value: PersistenceType) -> Self {
1174        match value {
1175            PersistenceType::Lapse => Self::Day,
1176            PersistenceType::Persist => Self::Gtc,
1177            PersistenceType::MarketOnClose => Self::AtTheClose,
1178        }
1179    }
1180}
1181
1182impl From<StreamingPersistenceType> for TimeInForce {
1183    fn from(value: StreamingPersistenceType) -> Self {
1184        match value {
1185            StreamingPersistenceType::Lapse => Self::Day,
1186            StreamingPersistenceType::Persist => Self::Gtc,
1187            StreamingPersistenceType::MarketOnClose => Self::AtTheClose,
1188        }
1189    }
1190}
1191
1192#[cfg(test)]
1193mod tests {
1194    use rstest::rstest;
1195
1196    use super::*;
1197
1198    #[rstest]
1199    fn test_reference_enums_tolerate_unmodeled_values() {
1200        // Market reference enums must degrade gracefully on a new venue value
1201        // rather than hard-fail deserialization of the streaming definition.
1202        assert_eq!(
1203            serde_json::from_str::<MarketStatus>("\"NEW_STATUS\"").unwrap(),
1204            MarketStatus::Unknown
1205        );
1206        assert_eq!(
1207            serde_json::from_str::<MarketBettingType>("\"NEW_TYPE\"").unwrap(),
1208            MarketBettingType::Unknown
1209        );
1210        assert_eq!(
1211            serde_json::from_str::<PriceLadderType>("\"NEW_LADDER\"").unwrap(),
1212            PriceLadderType::Unknown
1213        );
1214        assert_eq!(
1215            serde_json::from_str::<RunnerStatus>("\"NEW_RUNNER\"").unwrap(),
1216            RunnerStatus::Unknown
1217        );
1218    }
1219
1220    #[rstest]
1221    #[case(BetfairSide::Back, OrderSide::Sell)]
1222    #[case(BetfairSide::Lay, OrderSide::Buy)]
1223    fn test_betfair_side_to_order_side(#[case] input: BetfairSide, #[case] expected: OrderSide) {
1224        assert_eq!(OrderSide::from(input), expected);
1225    }
1226
1227    #[rstest]
1228    #[case(OrderSide::Buy, BetfairSide::Lay)]
1229    #[case(OrderSide::Sell, BetfairSide::Back)]
1230    fn test_order_side_to_betfair_side(#[case] input: OrderSide, #[case] expected: BetfairSide) {
1231        assert_eq!(BetfairSide::from(input), expected);
1232    }
1233
1234    #[rstest]
1235    #[should_panic(expected = "Invalid `OrderSide`")]
1236    fn test_order_side_no_order_side_panics() {
1237        let _ = BetfairSide::from(OrderSide::NoOrderSide);
1238    }
1239
1240    #[rstest]
1241    #[case(StreamingSide::Back, OrderSide::Sell)]
1242    #[case(StreamingSide::Lay, OrderSide::Buy)]
1243    fn test_streaming_side_to_order_side(
1244        #[case] input: StreamingSide,
1245        #[case] expected: OrderSide,
1246    ) {
1247        assert_eq!(OrderSide::from(input), expected);
1248    }
1249
1250    #[rstest]
1251    #[case(BetfairOrderType::Limit, OrderType::Limit)]
1252    #[case(BetfairOrderType::LimitOnClose, OrderType::Limit)]
1253    #[case(BetfairOrderType::MarketOnClose, OrderType::Market)]
1254    #[case(BetfairOrderType::MarketAtTheClose, OrderType::Market)]
1255    fn test_betfair_order_type(#[case] input: BetfairOrderType, #[case] expected: OrderType) {
1256        assert_eq!(OrderType::from(input), expected);
1257    }
1258
1259    #[rstest]
1260    #[case(StreamingOrderType::Limit, OrderType::Limit)]
1261    #[case(StreamingOrderType::LimitOnClose, OrderType::Limit)]
1262    #[case(StreamingOrderType::MarketOnClose, OrderType::Market)]
1263    fn test_streaming_order_type(#[case] input: StreamingOrderType, #[case] expected: OrderType) {
1264        assert_eq!(OrderType::from(input), expected);
1265    }
1266
1267    #[rstest]
1268    fn test_resolve_order_status_non_terminal() {
1269        assert_eq!(
1270            resolve_order_status(BetfairOrderStatus::Pending, Decimal::ZERO, Decimal::ZERO),
1271            OrderStatus::Submitted,
1272        );
1273        assert_eq!(
1274            resolve_order_status(BetfairOrderStatus::Executable, Decimal::ZERO, Decimal::ZERO),
1275            OrderStatus::Accepted,
1276        );
1277        assert_eq!(
1278            resolve_order_status(BetfairOrderStatus::Expired, Decimal::ZERO, Decimal::ZERO),
1279            OrderStatus::Expired,
1280        );
1281    }
1282
1283    #[rstest]
1284    fn test_resolve_order_status_executable_partially_matched() {
1285        assert_eq!(
1286            resolve_order_status(
1287                BetfairOrderStatus::Executable,
1288                Decimal::new(5, 0),
1289                Decimal::ZERO
1290            ),
1291            OrderStatus::PartiallyFilled,
1292        );
1293    }
1294
1295    #[rstest]
1296    #[case(Decimal::TEN, Decimal::ZERO, OrderStatus::Filled)]
1297    #[case(Decimal::new(5, 0), Decimal::new(5, 0), OrderStatus::Canceled)]
1298    #[case(Decimal::ZERO, Decimal::TEN, OrderStatus::Canceled)]
1299    fn test_resolve_order_status_execution_complete(
1300        #[case] size_matched: Decimal,
1301        #[case] size_cancelled: Decimal,
1302        #[case] expected: OrderStatus,
1303    ) {
1304        assert_eq!(
1305            resolve_order_status(
1306                BetfairOrderStatus::ExecutionComplete,
1307                size_matched,
1308                size_cancelled,
1309            ),
1310            expected,
1311        );
1312    }
1313
1314    #[rstest]
1315    fn test_resolve_streaming_order_status_executable() {
1316        assert_eq!(
1317            resolve_streaming_order_status(
1318                StreamingOrderStatus::Executable,
1319                Decimal::ZERO,
1320                Decimal::ZERO,
1321            ),
1322            OrderStatus::Accepted,
1323        );
1324    }
1325
1326    #[rstest]
1327    fn test_resolve_streaming_order_status_executable_partially_matched() {
1328        assert_eq!(
1329            resolve_streaming_order_status(
1330                StreamingOrderStatus::Executable,
1331                Decimal::new(5, 0),
1332                Decimal::ZERO,
1333            ),
1334            OrderStatus::PartiallyFilled,
1335        );
1336    }
1337
1338    #[rstest]
1339    #[case(Decimal::TEN, Decimal::ZERO, OrderStatus::Filled)]
1340    #[case(Decimal::new(5, 0), Decimal::new(5, 0), OrderStatus::Canceled)]
1341    #[case(Decimal::ZERO, Decimal::TEN, OrderStatus::Canceled)]
1342    fn test_resolve_streaming_order_status_execution_complete(
1343        #[case] size_matched: Decimal,
1344        #[case] size_cancelled: Decimal,
1345        #[case] expected: OrderStatus,
1346    ) {
1347        assert_eq!(
1348            resolve_streaming_order_status(
1349                StreamingOrderStatus::ExecutionComplete,
1350                size_matched,
1351                size_cancelled,
1352            ),
1353            expected,
1354        );
1355    }
1356
1357    #[rstest]
1358    #[case(MarketStatus::Open, NautilusMarketStatus::Open)]
1359    #[case(MarketStatus::Closed, NautilusMarketStatus::Closed)]
1360    #[case(MarketStatus::Suspended, NautilusMarketStatus::Suspended)]
1361    #[case(MarketStatus::Inactive, NautilusMarketStatus::NotAvailable)]
1362    fn test_market_status(#[case] input: MarketStatus, #[case] expected: NautilusMarketStatus) {
1363        assert_eq!(NautilusMarketStatus::from(input), expected);
1364    }
1365
1366    #[rstest]
1367    fn test_betfair_time_in_force() {
1368        assert_eq!(
1369            TimeInForce::from(BetfairTimeInForce::FillOrKill),
1370            TimeInForce::Fok
1371        );
1372    }
1373
1374    #[rstest]
1375    #[case(PersistenceType::Lapse, TimeInForce::Day)]
1376    #[case(PersistenceType::Persist, TimeInForce::Gtc)]
1377    #[case(PersistenceType::MarketOnClose, TimeInForce::AtTheClose)]
1378    fn test_persistence_type_to_time_in_force(
1379        #[case] input: PersistenceType,
1380        #[case] expected: TimeInForce,
1381    ) {
1382        assert_eq!(TimeInForce::from(input), expected);
1383    }
1384
1385    #[rstest]
1386    #[case(StreamingPersistenceType::Lapse, TimeInForce::Day)]
1387    #[case(StreamingPersistenceType::Persist, TimeInForce::Gtc)]
1388    #[case(StreamingPersistenceType::MarketOnClose, TimeInForce::AtTheClose)]
1389    fn test_streaming_persistence_type_to_time_in_force(
1390        #[case] input: StreamingPersistenceType,
1391        #[case] expected: TimeInForce,
1392    ) {
1393        assert_eq!(TimeInForce::from(input), expected);
1394    }
1395
1396    #[rstest]
1397    fn test_resolve_streaming_lapsed_and_voided_count_as_closed() {
1398        // size_closed includes lapsed + voided, so these should resolve to Canceled
1399        // even if size_cancelled itself is zero (the caller aggregates them)
1400        assert_eq!(
1401            resolve_streaming_order_status(
1402                StreamingOrderStatus::ExecutionComplete,
1403                Decimal::ZERO,
1404                Decimal::new(5, 0), // aggregated lapsed/voided/cancelled
1405            ),
1406            OrderStatus::Canceled,
1407        );
1408    }
1409
1410    #[rstest]
1411    fn test_resolve_streaming_partial_match_then_cancel() {
1412        // Partially matched then remainder cancelled
1413        assert_eq!(
1414            resolve_streaming_order_status(
1415                StreamingOrderStatus::ExecutionComplete,
1416                Decimal::new(3, 0), // matched
1417                Decimal::new(7, 0), // cancelled remainder
1418            ),
1419            OrderStatus::Canceled,
1420        );
1421    }
1422
1423    #[rstest]
1424    #[case(StatusErrorCode::NoAppKey, true)]
1425    #[case(StatusErrorCode::InvalidAppKey, true)]
1426    #[case(StatusErrorCode::NotAuthorized, true)]
1427    #[case(StatusErrorCode::SubscriptionLimitExceeded, true)]
1428    #[case(StatusErrorCode::MaxConnectionLimitExceeded, true)]
1429    #[case(StatusErrorCode::InvalidClock, false)]
1430    #[case(StatusErrorCode::Timeout, false)]
1431    #[case(StatusErrorCode::InvalidInput, false)]
1432    #[case(StatusErrorCode::TooManyRequests, false)]
1433    fn test_is_race_stream_fatal(#[case] code: StatusErrorCode, #[case] expected: bool) {
1434        assert_eq!(code.is_race_stream_fatal(), expected);
1435    }
1436}