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