rustyfix_dictionary/fix_datatype.rs
1use strum::IntoEnumIterator;
2use strum_macros::{EnumIter, IntoStaticStr};
3
4/// Sum type for all possible FIX data types ever defined across all FIX
5/// application versions.
6#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, EnumIter, IntoStaticStr)]
7#[repr(u8)]
8#[non_exhaustive]
9pub enum FixDatatype {
10 /// Single character value, can include any alphanumeric character or
11 /// punctuation except the delimiter. All char fields are case sensitive
12 /// (i.e. m != M). The following fields are based on char.
13 Char,
14 /// char field containing one of two values: 'Y' = True/Yes 'N' = False/No.
15 Boolean,
16 /// Sequence of digits with optional decimal point and sign character (ASCII
17 /// characters "-", "0" - "9" and "."); the absence of the decimal point
18 /// within the string will be interpreted as the float representation of an
19 /// integer value. All float fields must accommodate up to fifteen
20 /// significant digits. The number of decimal places used should be a factor
21 /// of business/market needs and mutual agreement between counterparties.
22 /// Note that float values may contain leading zeros (e.g. "00023.23" =
23 /// "23.23") and may contain or omit trailing zeros after the decimal point
24 /// (e.g. "23.0" = "23.0000" = "23" = "23."). Note that fields which are
25 /// derived from float may contain negative values unless explicitly
26 /// specified otherwise. The following data types are based on float.
27 Float,
28 /// float field typically representing a Price times a Qty.
29 Amt,
30 /// float field representing a price. Note the number of decimal places may
31 /// vary. For certain asset classes prices may be negative values. For
32 /// example, prices for options strategies can be negative under certain
33 /// market conditions. Refer to Volume 7: FIX Usage by Product for asset
34 /// classes that support negative price values.
35 Price,
36 /// float field representing a price offset, which can be mathematically
37 /// added to a "Price". Note the number of decimal places may vary and some
38 /// fields such as LastForwardPoints may be negative.
39 PriceOffset,
40 /// float field capable of storing either a whole number (no decimal places)
41 /// of "shares" (securities denominated in whole units) or a decimal value
42 /// containing decimal places for non-share quantity asset classes
43 /// (securities denominated in fractional units).
44 Quantity,
45 /// float field representing a percentage (e.g. 0.05 represents 5% and 0.9525
46 /// represents 95.25%). Note the number of decimal places may vary.
47 Percentage,
48 /// Sequence of digits without commas or decimals and optional sign character
49 /// (ASCII characters "-" and "0" - "9" ). The sign character utilizes one
50 /// byte (i.e. positive int is "99999" while negative int is "-99999"). Note
51 /// that int values may contain leading zeros (e.g. "00023" = "23").
52 /// Examples: 723 in field 21 would be mapped int as |21=723|. -723 in field
53 /// 12 would be mapped int as |12=-723| The following data types are based on
54 /// int.
55 Int,
56 /// int field representing a day during a particular monthy (values 1 to 31).
57 DayOfMonth,
58 /// int field representing the length in bytes. Value must be positive.
59 Length,
60 /// int field representing the number of entries in a repeating group. Value
61 /// must be positive.
62 NumInGroup,
63 /// int field representing a message sequence number. Value must be positive.
64 SeqNum,
65 /// `int` field representing a field's tag number when using FIX "Tag=Value"
66 /// syntax. Value must be positive and may not contain leading zeros.
67 TagNum,
68 /// Alpha-numeric free format strings, can include any character or
69 /// punctuation except the delimiter. All String fields are case sensitive
70 /// (i.e. morstatt != Morstatt).
71 String,
72 /// string field containing raw data with no format or content restrictions.
73 /// Data fields are always immediately preceded by a length field. The length
74 /// field should specify the number of bytes of the value of the data field
75 /// (up to but not including the terminating SOH). Caution: the value of one
76 /// of these fields may contain the delimiter (SOH) character. Note that the
77 /// value specified for this field should be followed by the delimiter (SOH)
78 /// character as all fields are terminated with an "SOH".
79 Data,
80 /// string field representing month of a year. An optional day of the month
81 /// can be appended or an optional week code. Valid formats: YYYYMM YYYYMMDD
82 /// YYYYMMWW Valid values: YYYY = 0000-9999; MM = 01-12; DD = 01-31; WW = w1,
83 /// w2, w3, w4, w5.
84 MonthYear,
85 /// string field containing one or more space delimited single character
86 /// values (e.g. |18=2 A F| ).
87 MultipleCharValue,
88 /// string field representing a currency type using ISO 4217 Currency code (3
89 /// character) values (see Appendix 6-A).
90 Currency,
91 /// string field representing a market or exchange using ISO 10383 Market
92 /// Identifier Code (MIC) values (see"Appendix 6-C).
93 Exchange,
94 /// Identifier for a national language - uses ISO 639-1 standard.
95 Language,
96 /// string field represening a Date of Local Market (as oppose to UTC) in
97 /// YYYYMMDD format. This is the "normal" date field used by the FIX
98 /// Protocol. Valid values: YYYY = 0000-9999, MM = 01-12, DD = 01-31.
99 LocalMktDate,
100 /// string field containing one or more space delimited multiple character
101 /// values (e.g. |277=AV AN A| ).
102 MultipleStringValue,
103 /// string field representing Date represented in UTC (Universal Time
104 /// Coordinated, also known as "GMT") in YYYYMMDD format. This
105 /// special-purpose field is paired with UTCTimeOnly to form a proper
106 /// UTCTimestamp for bandwidth-sensitive messages. Valid values: YYYY =
107 /// 0000-9999, MM = 01-12, DD = 01-31.
108 UtcDateOnly,
109 /// string field representing Time-only represented in UTC (Universal Time
110 /// Coordinated, also known as "GMT") in either HH:MM:SS (whole seconds) or
111 /// HH:MM:SS.sss (milliseconds) format, colons, and period required. This
112 /// special-purpose field is paired with UTCDateOnly to form a proper
113 /// UTCTimestamp for bandwidth-sensitive messages. Valid values: HH = 00-23,
114 /// MM = 00-60 (60 only if UTC leap second), SS = 00-59. (without
115 /// milliseconds) HH = 00-23, MM = 00-59, SS = 00-60 (60 only if UTC leap
116 /// second), sss=000-999 (indicating milliseconds).
117 UtcTimeOnly,
118 /// string field representing Time/date combination represented in UTC
119 /// (Universal Time Coordinated, also known as "GMT") in either
120 /// YYYYMMDD-HH:MM:SS (whole seconds) or YYYYMMDD-HH:MM:SS.sss (milliseconds)
121 /// format, colons, dash, and period required. Valid values: * YYYY =
122 /// 0000-9999, MM = 01-12, DD = 01-31, HH = 00-23, MM = 00-59, SS = 00-60 (60
123 /// only if UTC leap second) (without milliseconds). * YYYY = 0000-9999, MM =
124 /// 01-12, DD = 01-31, HH = 00-23, MM = 00-59, SS = 00-60 (60 only if UTC
125 /// leap second), sss=000-999 (indicating milliseconds). Leap Seconds: Note
126 /// that UTC includes corrections for leap seconds, which are inserted to
127 /// account for slowing of the rotation of the earth. Leap second insertion
128 /// is declared by the International Earth Rotation Service (IERS) and has,
129 /// since 1972, only occurred on the night of Dec. 31 or Jun 30. The IERS
130 /// considers March 31 and September 30 as secondary dates for leap second
131 /// insertion, but has never utilized these dates. During a leap second
132 /// insertion, a UTCTimestamp field may read "19981231-23:59:59",
133 /// "19981231-23:59:60", "19990101-00:00:00". (see
134 /// <http://tycho.usno.navy.mil/leapsec.html>)
135 UtcTimestamp,
136 /// Contains an XML document raw data with no format or content restrictions.
137 /// XMLData fields are always immediately preceded by a length field. The
138 /// length field should specify the number of bytes of the value of the data
139 /// field (up to but not including the terminating SOH).
140 XmlData,
141 /// string field representing a country using ISO 3166 Country code (2
142 /// character) values (see Appendix 6-B).
143 Country,
144}
145
146impl FixDatatype {
147 /// Compares `name` to the set of strings commonly used by QuickFIX's custom
148 /// specification format and returns its associated
149 /// [`Datatype`](super::Datatype) if a match
150 /// was found. The query is case-insensitive.
151 ///
152 /// # Examples
153 ///
154 /// ```
155 /// use rustyfix_dictionary::FixDatatype;
156 ///
157 /// assert_eq!(FixDatatype::from_quickfix_name("AMT"), Some(FixDatatype::Amt));
158 /// assert_eq!(FixDatatype::from_quickfix_name("Amt"), Some(FixDatatype::Amt));
159 /// assert_eq!(FixDatatype::from_quickfix_name("MONTHYEAR"), Some(FixDatatype::MonthYear));
160 /// assert_eq!(FixDatatype::from_quickfix_name(""), None);
161 /// ```
162 pub fn from_quickfix_name(name: &str) -> Option<Self> {
163 // https://github.com/quickfix/quickfix/blob/b6760f55ac6a46306b4e081bb13b65e6220ab02d/src/C%2B%2B/DataDictionary.cpp#L646-L680
164 Some(match name.to_ascii_uppercase().as_str() {
165 "AMT" => FixDatatype::Amt,
166 "BOOLEAN" => FixDatatype::Boolean,
167 "CHAR" => FixDatatype::Char,
168 "COUNTRY" => FixDatatype::Country,
169 "CURRENCY" => FixDatatype::Currency,
170 "DATA" => FixDatatype::Data,
171 "DATE" => FixDatatype::UtcDateOnly, // FIXME?
172 "DAYOFMONTH" => FixDatatype::DayOfMonth,
173 "EXCHANGE" => FixDatatype::Exchange,
174 "FLOAT" => FixDatatype::Float,
175 "INT" => FixDatatype::Int,
176 "LANGUAGE" => FixDatatype::Language,
177 "LENGTH" => FixDatatype::Length,
178 "LOCALMKTDATE" => FixDatatype::LocalMktDate,
179 "MONTHYEAR" => FixDatatype::MonthYear,
180 "MULTIPLECHARVALUE" | "MULTIPLEVALUESTRING" => FixDatatype::MultipleCharValue,
181 "MULTIPLESTRINGVALUE" => FixDatatype::MultipleStringValue,
182 "NUMINGROUP" => FixDatatype::NumInGroup,
183 "PERCENTAGE" => FixDatatype::Percentage,
184 "PRICE" => FixDatatype::Price,
185 "PRICEOFFSET" => FixDatatype::PriceOffset,
186 "QTY" => FixDatatype::Quantity,
187 "STRING" => FixDatatype::String,
188 "TZTIMEONLY" => FixDatatype::UtcTimeOnly, // FIXME
189 "TZTIMESTAMP" => FixDatatype::UtcTimestamp, // FIXME
190 "UTCDATE" => FixDatatype::UtcDateOnly,
191 "UTCDATEONLY" => FixDatatype::UtcDateOnly,
192 "UTCTIMEONLY" => FixDatatype::UtcTimeOnly,
193 "UTCTIMESTAMP" => FixDatatype::UtcTimestamp,
194 "SEQNUM" => FixDatatype::SeqNum,
195 "TIME" => FixDatatype::UtcTimestamp,
196 "XMLDATA" => FixDatatype::XmlData,
197 _ => {
198 return None;
199 }
200 })
201 }
202
203 /// Returns the name adopted by QuickFIX for `self`.
204 pub fn to_quickfix_name(&self) -> &str {
205 match self {
206 FixDatatype::Int => "INT",
207 FixDatatype::Length => "LENGTH",
208 FixDatatype::Char => "CHAR",
209 FixDatatype::Boolean => "BOOLEAN",
210 FixDatatype::Float => "FLOAT",
211 FixDatatype::Amt => "AMT",
212 FixDatatype::Price => "PRICE",
213 FixDatatype::PriceOffset => "PRICEOFFSET",
214 FixDatatype::Quantity => "QTY",
215 FixDatatype::Percentage => "PERCENTAGE",
216 FixDatatype::DayOfMonth => "DAYOFMONTH",
217 FixDatatype::NumInGroup => "NUMINGROUP",
218 FixDatatype::Language => "LANGUAGE",
219 FixDatatype::SeqNum => "SEQNUM",
220 FixDatatype::TagNum => "TAGNUM",
221 FixDatatype::String => "STRING",
222 FixDatatype::Data => "DATA",
223 FixDatatype::MonthYear => "MONTHYEAR",
224 FixDatatype::Currency => "CURRENCY",
225 FixDatatype::Exchange => "EXCHANGE",
226 FixDatatype::LocalMktDate => "LOCALMKTDATE",
227 FixDatatype::MultipleStringValue => "MULTIPLESTRINGVALUE",
228 FixDatatype::UtcTimeOnly => "UTCTIMEONLY",
229 FixDatatype::UtcTimestamp => "UTCTIMESTAMP",
230 FixDatatype::UtcDateOnly => "UTCDATEONLY",
231 FixDatatype::Country => "COUNTRY",
232 FixDatatype::MultipleCharValue => "MULTIPLECHARVALUE",
233 FixDatatype::XmlData => "XMLDATA",
234 }
235 }
236
237 /// Returns the name of `self`, character by character identical to the name
238 /// that appears in the official guidelines. **Generally** primitive datatypes
239 /// will use `snake_case` and non-primitive ones will have `PascalCase`, but
240 /// that's not true for every [`Datatype`](super::Datatype).
241 ///
242 /// # Examples
243 ///
244 /// ```
245 /// use rustyfix_dictionary::FixDatatype;
246 ///
247 /// assert_eq!(FixDatatype::Quantity.name(), "Quantity");
248 /// assert_eq!(FixDatatype::Float.name(), "float");
249 /// assert_eq!(FixDatatype::String.name(), "String");
250 /// ```
251 pub fn name(&self) -> &'static str {
252 // 1. Most primitive data types have `snake_case` names.
253 // 2. Most derivative data types have `PascalCase` names.
254 // 3. `data` and `String` ruin the party and mess it up.
255 // Why, you ask? Oh, you sweet summer child. You'll learn soon enough
256 // that nothing makes sense in FIX land.
257 match self {
258 FixDatatype::Int => "int",
259 FixDatatype::Length => "Length",
260 FixDatatype::Char => "char",
261 FixDatatype::Boolean => "Boolean",
262 FixDatatype::Float => "float",
263 FixDatatype::Amt => "Amt",
264 FixDatatype::Price => "Price",
265 FixDatatype::PriceOffset => "PriceOffset",
266 FixDatatype::Quantity => "Quantity",
267 FixDatatype::Percentage => "Percentage",
268 FixDatatype::DayOfMonth => "DayOfMonth",
269 FixDatatype::NumInGroup => "NumInGroup",
270 FixDatatype::Language => "Language",
271 FixDatatype::SeqNum => "SeqNum",
272 FixDatatype::TagNum => "TagNum",
273 FixDatatype::String => "String",
274 FixDatatype::Data => "data",
275 FixDatatype::MonthYear => "MonthYear",
276 FixDatatype::Currency => "Currency",
277 FixDatatype::Exchange => "Exchange",
278 FixDatatype::LocalMktDate => "LocalMktDate",
279 FixDatatype::MultipleStringValue => "MultipleStringValue",
280 FixDatatype::UtcTimeOnly => "UTCTimeOnly",
281 FixDatatype::UtcTimestamp => "UTCTimestamp",
282 FixDatatype::UtcDateOnly => "UTCDateOnly",
283 FixDatatype::Country => "Country",
284 FixDatatype::MultipleCharValue => "MultipleCharValue",
285 FixDatatype::XmlData => "XMLData",
286 }
287 }
288
289 /// Returns `true` if and only if `self` is a "base type", i.e. a primitive;
290 /// returns `false` otherwise.
291 ///
292 /// # Examples
293 ///
294 /// ```
295 /// use rustyfix_dictionary::FixDatatype;
296 ///
297 /// assert_eq!(FixDatatype::Float.is_base_type(), true);
298 /// assert_eq!(FixDatatype::Price.is_base_type(), false);
299 /// ```
300 pub fn is_base_type(&self) -> bool {
301 matches!(self, Self::Char | Self::Float | Self::Int | Self::String)
302 }
303
304 /// Returns the primitive [`Datatype`](super::Datatype) from which `self` is derived. If
305 /// `self` is primitive already, returns `self` unchanged.
306 ///
307 /// # Examples
308 ///
309 /// ```
310 /// use rustyfix_dictionary::FixDatatype;
311 ///
312 /// assert_eq!(FixDatatype::Float.base_type(), FixDatatype::Float);
313 /// assert_eq!(FixDatatype::Price.base_type(), FixDatatype::Float);
314 /// ```
315 pub fn base_type(&self) -> Self {
316 let dt = match self {
317 Self::Char | Self::Boolean => Self::Char,
318 Self::Float
319 | Self::Amt
320 | Self::Price
321 | Self::PriceOffset
322 | Self::Quantity
323 | Self::Percentage => Self::Float,
324 Self::Int
325 | Self::DayOfMonth
326 | Self::Length
327 | Self::NumInGroup
328 | Self::SeqNum
329 | Self::TagNum => Self::Int,
330 _ => Self::String,
331 };
332 debug_assert!(dt.is_base_type());
333 dt
334 }
335
336 /// Returns an [`Iterator`] over all variants of
337 /// [`Datatype`](super::Datatype).
338 pub fn iter_all() -> impl Iterator<Item = Self> {
339 <Self as IntoEnumIterator>::iter()
340 }
341}
342
343#[cfg(test)]
344mod test {
345 use super::*;
346 use rustc_hash::FxHashSet;
347
348 #[test]
349 fn iter_all_unique() {
350 let as_vec = FixDatatype::iter_all().collect::<Vec<FixDatatype>>();
351 let as_set = FixDatatype::iter_all().collect::<FxHashSet<FixDatatype>>();
352 assert_eq!(as_vec.len(), as_set.len());
353 }
354
355 #[test]
356 fn more_than_20_datatypes() {
357 // According to the official documentation, FIX has "about 20 data
358 // types". Including recent revisions, we should well exceed that
359 // number.
360 assert!(FixDatatype::iter_all().count() > 20);
361 }
362
363 #[test]
364 fn names_are_unique() {
365 let as_vec = FixDatatype::iter_all()
366 .map(|dt| dt.name())
367 .collect::<Vec<&str>>();
368 let as_set = FixDatatype::iter_all()
369 .map(|dt| dt.name())
370 .collect::<FxHashSet<&str>>();
371 assert_eq!(as_vec.len(), as_set.len());
372 }
373
374 #[test]
375 fn base_type_is_itself() {
376 for dt in FixDatatype::iter_all() {
377 if dt.is_base_type() {
378 assert_eq!(dt.base_type(), dt);
379 } else {
380 assert_ne!(dt.base_type(), dt);
381 }
382 }
383 }
384
385 #[test]
386 fn base_type_is_actually_base_type() {
387 for dt in FixDatatype::iter_all() {
388 assert!(dt.base_type().is_base_type());
389 }
390 }
391}