Skip to main content

optionstratlib_core/model/
format.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 20/8/24
5******************************************************************************/
6use crate::model::option::ExoticParams;
7use crate::model::{Options, Position};
8use positive::Positive;
9use rust_decimal::Decimal;
10use std::fmt;
11
12/// The label shown in place of an amount that stands for "unlimited".
13pub const UNLIMITED: &str = "Unlimited";
14
15/// The label shown in place of an amount that stands for "unlimited",
16/// if `value` is one of the sentinels the library uses for it.
17///
18/// `Decimal::MAX` (which is also `Positive::MAX`, the value the strategies'
19/// `get_max_profit` / `get_max_loss` return for an unbounded side) gives
20/// `Unlimited`, and `Decimal::MIN` gives `-Unlimited`. Every other value,
21/// however large, gives `None`.
22#[inline]
23#[must_use]
24pub fn unlimited_label(value: Decimal) -> Option<&'static str> {
25    if value == Decimal::MAX {
26        Some(UNLIMITED)
27    } else if value == Decimal::MIN {
28        Some("-Unlimited")
29    } else {
30        None
31    }
32}
33
34/// A monetary amount as shown to a user: `$` followed by the amount, or the
35/// [`unlimited_label`] when the amount is an "unlimited" sentinel.
36///
37/// Display only: the wrapped value is not changed. The formatter's precision
38/// and width apply to the number only, so `format!("{:.2}", DisplayMoney(x))`
39/// prints `$12.34` for a finite amount and `Unlimited` for `Decimal::MAX`.
40///
41/// ```
42/// use optionstratlib_core::model::DisplayMoney;
43/// use positive::Positive;
44/// use rust_decimal_macros::dec;
45///
46/// assert_eq!(format!("{:.2}", DisplayMoney(dec!(12.34))), "$12.34");
47/// assert_eq!(DisplayMoney::from(Positive::MAX).to_string(), "Unlimited");
48/// ```
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct DisplayMoney(pub Decimal);
51
52impl From<Decimal> for DisplayMoney {
53    #[inline]
54    fn from(value: Decimal) -> Self {
55        Self(value)
56    }
57}
58
59/// Normalised, so a `Positive` printed without a precision keeps the
60/// trailing-zero-free rendering of `Positive`'s own `Display`.
61impl From<Positive> for DisplayMoney {
62    #[inline]
63    fn from(value: Positive) -> Self {
64        Self(value.to_dec().normalize())
65    }
66}
67
68impl fmt::Display for DisplayMoney {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match unlimited_label(self.0) {
71            // Not `pad`: a precision would truncate the label.
72            Some(label) => f.write_str(label),
73            None => {
74                f.write_str("$")?;
75                fmt::Display::fmt(&self.0, f)
76            }
77        }
78    }
79}
80
81/// Writes `fraction` as a percentage with two decimals. A fraction whose
82/// percentage leaves the `Decimal` range is written as the fraction itself,
83/// marked as such; the multiplication aborted on it (#788).
84fn write_percentage(f: &mut fmt::Formatter<'_>, fraction: Decimal) -> fmt::Result {
85    match fraction.checked_mul(Decimal::ONE_HUNDRED) {
86        Some(percent) => write!(f, "{percent:.2}%"),
87        None => write!(f, "{fraction} (fraction)"),
88    }
89}
90
91impl fmt::Display for Options {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        writeln!(
94            f,
95            "{} {} {}",
96            self.side, self.option_style, self.option_type
97        )?;
98        writeln!(
99            f,
100            "Underlying: {} @ ${:.2}",
101            self.underlying_symbol, self.underlying_price
102        )?;
103        writeln!(f, "Strike: ${:.2}", self.strike_price)?;
104        writeln!(f, "Expiration: {}", self.expiration_date)?;
105        write!(f, "Implied Volatility: ")?;
106        write_percentage(f, self.implied_volatility.to_dec())?;
107        writeln!(f)?;
108        writeln!(f, "Quantity: {}", self.quantity)?;
109        // Shown only when it departs from the default of one unit per
110        // contract, so the rendering of every pre-existing option is unchanged.
111        if self.contract_size != Positive::ONE {
112            writeln!(f, "Contract Size: {}", self.contract_size)?;
113        }
114        write!(f, "Risk-free Rate: ")?;
115        write_percentage(f, self.risk_free_rate)?;
116        writeln!(f)?;
117        write!(f, "Dividend Yield: ")?;
118        write_percentage(f, self.dividend_yield.to_dec())?;
119        if let Some(exotic) = &self.exotic_params {
120            write!(f, "\nExotic Parameters: {exotic:?}")?;
121        }
122        Ok(())
123    }
124}
125
126impl fmt::Debug for Options {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        let mut debug = f.debug_struct("Options");
129        debug
130            .field("option_type", &self.option_type)
131            .field("side", &self.side)
132            .field("underlying_symbol", &self.underlying_symbol)
133            .field("strike_price", &self.strike_price)
134            .field("expiration_date", &self.expiration_date)
135            .field("implied_volatility", &self.implied_volatility)
136            .field("quantity", &self.quantity);
137        // Shown only when it departs from the default of one unit per
138        // contract, so the rendering of every pre-existing option is unchanged.
139        if self.contract_size != Positive::ONE {
140            debug.field("contract_size", &self.contract_size);
141        }
142        debug
143            .field("underlying_price", &self.underlying_price)
144            .field("risk_free_rate", &self.risk_free_rate)
145            .field("option_style", &self.option_style)
146            .field("dividend_yield", &self.dividend_yield)
147            .field("exotic_params", &self.exotic_params)
148            .finish()
149    }
150}
151
152impl fmt::Display for ExoticParams {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        let mut fields = vec![];
155
156        if let Some(ref prices) = self.spot_prices {
157            fields.push(format!("Spot Prices: {prices:?}"));
158        }
159
160        if let Some(min) = self.spot_min {
161            fields.push(format!("Spot Min: {min:.2}"));
162        }
163
164        if let Some(max) = self.spot_max {
165            fields.push(format!("Spot Max: {max:.2}"));
166        }
167
168        if let Some(cap) = self.cliquet_local_cap {
169            fields.push(format!("Cliquet Local Cap: {cap:.2}"));
170        }
171
172        if let Some(floor) = self.cliquet_local_floor {
173            fields.push(format!("Cliquet Local Floor: {floor:.2}"));
174        }
175
176        if let Some(cap) = self.cliquet_global_cap {
177            fields.push(format!("Cliquet Global Cap: {cap:.2}"));
178        }
179
180        if let Some(floor) = self.cliquet_global_floor {
181            fields.push(format!("Cliquet Global Floor: {floor:.2}"));
182        }
183
184        if let Some(ref price) = self.rainbow_second_asset_price {
185            fields.push(format!("Rainbow Second Asset Price: {price}"));
186        }
187
188        if let Some(ref vol) = self.rainbow_second_asset_volatility {
189            fields.push(format!("Rainbow Second Asset Volatility: {vol}"));
190        }
191
192        if let Some(ref div) = self.rainbow_second_asset_dividend {
193            fields.push(format!("Rainbow Second Asset Dividend: {div}"));
194        }
195
196        if let Some(corr) = self.rainbow_correlation {
197            fields.push(format!("Rainbow Correlation: {corr:.4}"));
198        }
199
200        if let Some(ref vol) = self.spread_second_asset_volatility {
201            fields.push(format!("Spread Second Asset Volatility: {vol}"));
202        }
203
204        if let Some(ref div) = self.spread_second_asset_dividend {
205            fields.push(format!("Spread Second Asset Dividend: {div}"));
206        }
207
208        if let Some(corr) = self.spread_correlation {
209            fields.push(format!("Spread Correlation: {corr:.4}"));
210        }
211
212        if let Some(ref vol) = self.quanto_fx_volatility {
213            fields.push(format!("Quanto FX Volatility: {vol}"));
214        }
215
216        if let Some(corr) = self.quanto_fx_correlation {
217            fields.push(format!("Quanto FX Correlation: {corr:.4}"));
218        }
219
220        if let Some(rate) = self.quanto_foreign_rate {
221            fields.push(format!("Quanto Foreign Rate: {rate:.4}"));
222        }
223
224        if let Some(rate) = self.foreign_rate {
225            fields.push(format!("Foreign Rate: {rate:.4}"));
226        }
227
228        if let Some(ref vol) = self.exchange_second_asset_volatility {
229            fields.push(format!("Exchange Second Asset Volatility: {vol}"));
230        }
231
232        if let Some(ref div) = self.exchange_second_asset_dividend {
233            fields.push(format!("Exchange Second Asset Dividend: {div}"));
234        }
235
236        if let Some(corr) = self.exchange_correlation {
237            fields.push(format!("Exchange Correlation: {corr:.4}"));
238        }
239
240        write!(f, "{}", fields.join(", "))
241    }
242}
243
244impl fmt::Display for Position {
245    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246        writeln!(f, "Position Details:")?;
247        writeln!(f, "Option: {}", self.option)?;
248        writeln!(f, "Premium per contract: ${:.2}", self.premium)?;
249        writeln!(f, "Date: {}", self.date)?;
250        writeln!(f, "Open Fee per contract: ${:.2}", self.open_fee)?;
251        write!(f, "Close Fee per contract: ${:.2}", self.close_fee)
252    }
253}
254
255impl fmt::Debug for Position {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        f.debug_struct("Position")
258            .field("option", &self.option)
259            .field("premium", &self.premium)
260            .field("date", &self.date)
261            .field("open_fee", &self.open_fee)
262            .field("close_fee", &self.close_fee)
263            .finish()
264    }
265}
266
267#[cfg(test)]
268mod tests_options {
269    use super::*;
270    use crate::model::types::{BarrierType, OptionStyle, OptionType, Side};
271
272    use chrono::{NaiveDate, TimeZone, Utc};
273    use expiration_date::ExpirationDate;
274    use positive::{Positive, pos_or_panic};
275    use rust_decimal_macros::dec;
276
277    // `Positive * 100.0` aborted with `Positive arithmetic overflow in
278    // mul_f64` on a volatility above `Decimal::MAX / 100` (#788).
279    #[test]
280    fn test_display_options_out_of_range_percentages() {
281        let mut options =
282            crate::model::utils::create_sample_option_simplest(OptionStyle::Call, Side::Long);
283        options.implied_volatility = Positive::MAX;
284        options.dividend_yield = Positive::MAX;
285        options.risk_free_rate = Decimal::MIN;
286        let shown = options.to_string();
287        assert!(shown.contains(&format!("Implied Volatility: {} (fraction)", Positive::MAX)));
288        assert!(shown.contains(&format!("Risk-free Rate: {} (fraction)", Decimal::MIN)));
289        assert!(shown.contains(&format!("Dividend Yield: {} (fraction)", Positive::MAX)));
290    }
291
292    #[test]
293    fn test_debug_options() {
294        let naive_date = NaiveDate::from_ymd_opt(2024, 8, 8)
295            .expect("Invalid date")
296            .and_hms_opt(0, 0, 0)
297            .expect("Invalid time");
298
299        let options = Options {
300            option_type: OptionType::European,
301            side: Side::Long,
302            underlying_symbol: "AAPL".to_string(),
303            strike_price: pos_or_panic!(150.0),
304            expiration_date: ExpirationDate::DateTime(Utc.from_utc_datetime(&naive_date)),
305            implied_volatility: pos_or_panic!(0.25),
306            quantity: pos_or_panic!(10.0),
307            contract_size: Positive::ONE,
308            underlying_price: pos_or_panic!(155.0),
309            risk_free_rate: dec!(0.01),
310            option_style: OptionStyle::Call,
311            dividend_yield: pos_or_panic!(0.02),
312            exotic_params: None,
313        };
314
315        let debug_output = format!("{options:?}");
316        let expected_output = "Options { \
317            option_type: European, \
318            side: Side::Long, \
319            underlying_symbol: \"AAPL\", \
320            strike_price: 150, \
321            expiration_date: DateTime(2024-08-08T00:00:00Z), \
322            implied_volatility: 0.25, \
323            quantity: 10, \
324            underlying_price: 155, \
325            risk_free_rate: 0.01, \
326            option_style: OptionStyle::Call, \
327            dividend_yield: 0.02, \
328            exotic_params: None \
329        }";
330
331        assert_eq!(debug_output, expected_output);
332    }
333
334    #[test]
335    fn test_display_options() {
336        let naive_date = NaiveDate::from_ymd_opt(2024, 8, 8)
337            .expect("Invalid date")
338            .and_hms_opt(0, 0, 0)
339            .expect("Invalid time");
340
341        let options = Options {
342            option_type: OptionType::European,
343            side: Side::Long,
344            underlying_symbol: "AAPL".to_string(),
345            strike_price: pos_or_panic!(150.0),
346            expiration_date: ExpirationDate::DateTime(Utc.from_utc_datetime(&naive_date)),
347            implied_volatility: pos_or_panic!(0.25),
348            quantity: pos_or_panic!(10.0),
349            contract_size: Positive::ONE,
350            underlying_price: pos_or_panic!(155.0),
351            risk_free_rate: dec!(0.01),
352            option_style: OptionStyle::Call,
353            dividend_yield: pos_or_panic!(0.02),
354            exotic_params: None,
355        };
356
357        let display_output = format!("{options}");
358        let expected_output = "\
359            Long Call European Option\n\
360            Underlying: AAPL @ $155.00\n\
361            Strike: $150.00\n\
362            Expiration: 2024-08-08 00:00:00 UTC\n\
363            Implied Volatility: 25.00%\n\
364            Quantity: 10\n\
365            Risk-free Rate: 1.00%\n\
366            Dividend Yield: 2.00%";
367
368        assert_eq!(display_output, expected_output);
369    }
370
371    #[test]
372    fn test_display_exotic_params_shows_signed_foreign_rate() {
373        let params = ExoticParams {
374            foreign_rate: Some(rust_decimal::Decimal::new(-75, 4)),
375            ..ExoticParams::default()
376        };
377        assert_eq!(params.to_string(), "Foreign Rate: -0.0075");
378    }
379
380    #[test]
381    fn test_display_options_with_exotic_params() {
382        let exotic_params = ExoticParams {
383            spot_prices: None,
384            spot_min: None,
385            spot_max: None,
386            cliquet_local_cap: None,
387            cliquet_local_floor: None,
388            cliquet_global_cap: None,
389            cliquet_global_floor: None,
390            rainbow_second_asset_price: None,
391            rainbow_second_asset_volatility: None,
392            rainbow_second_asset_dividend: None,
393            rainbow_correlation: None,
394            spread_second_asset_volatility: None,
395            spread_second_asset_dividend: None,
396            spread_correlation: None,
397            quanto_fx_volatility: None,
398            quanto_fx_correlation: None,
399            quanto_foreign_rate: None,
400            foreign_rate: None,
401            exchange_second_asset_volatility: None,
402            exchange_second_asset_dividend: None,
403            exchange_correlation: None,
404        };
405        let naive_date = NaiveDate::from_ymd_opt(2024, 8, 8)
406            .expect("Invalid date")
407            .and_hms_opt(0, 0, 0)
408            .expect("Invalid time");
409
410        let options = Options {
411            option_type: OptionType::Barrier {
412                barrier_type: BarrierType::UpAndIn,
413                barrier_level: pos_or_panic!(100.0),
414                rebate: None,
415            },
416            side: Side::Short,
417            underlying_symbol: "GOOGL".to_string(),
418            strike_price: pos_or_panic!(2000.0),
419            expiration_date: ExpirationDate::DateTime(Utc.from_utc_datetime(&naive_date)),
420            implied_volatility: pos_or_panic!(0.30),
421            quantity: pos_or_panic!(5.0),
422            contract_size: Positive::ONE,
423            underlying_price: pos_or_panic!(1900.0),
424            risk_free_rate: dec!(0.015),
425            option_style: OptionStyle::Call,
426            dividend_yield: pos_or_panic!(0.01),
427            exotic_params: Some(exotic_params),
428        };
429
430        let display_output = format!("{options}");
431        let expected_output = "\
432            Short Call Barrier Option (Type: Up-And-In Barrier, Level: 100, Rebate: None)\n\
433            Underlying: GOOGL @ $1900.00\n\
434            Strike: $2000.00\n\
435            Expiration: 2024-08-08 00:00:00 UTC\n\
436            Implied Volatility: 30.00%\n\
437            Quantity: 5\n\
438            Risk-free Rate: 1.50%\n\
439            Dividend Yield: 1.00%\n\
440            Exotic Parameters: ExoticParams { spot_prices: None, spot_min: None, spot_max: None, cliquet_local_cap: None, cliquet_local_floor: None, cliquet_global_cap: None, cliquet_global_floor: None, rainbow_second_asset_price: None, rainbow_second_asset_volatility: None, rainbow_second_asset_dividend: None, rainbow_correlation: None, spread_second_asset_volatility: None, spread_second_asset_dividend: None, spread_correlation: None, quanto_fx_volatility: None, quanto_fx_correlation: None, quanto_foreign_rate: None, foreign_rate: None, exchange_second_asset_volatility: None, exchange_second_asset_dividend: None, exchange_correlation: None }";
441
442        assert_eq!(display_output, expected_output);
443    }
444}
445
446#[cfg(test)]
447mod tests_expiration_date {
448
449    use chrono::{Duration, NaiveDate, TimeZone, Utc};
450    use expiration_date::ExpirationDate;
451    use positive::pos_or_panic;
452    use tracing::info;
453
454    #[test]
455    fn test_display_days() {
456        let expiration = ExpirationDate::Days(pos_or_panic!(30.5));
457        let display_string = format!("{expiration}");
458        assert!(display_string.contains("UTC"));
459    }
460
461    #[test]
462    fn test_display_datetime() {
463        let future_date = Utc::now() + Duration::days(15) + Duration::minutes(1);
464        let expiration = ExpirationDate::DateTime(future_date);
465        let display_string = format!("{expiration}");
466        assert!(display_string.contains("UTC"));
467        assert!(display_string.contains(&future_date.format("%Y-%m-%d %H:%M:%S").to_string()));
468    }
469
470    #[test]
471    fn test_debug_days() {
472        let expiration = ExpirationDate::Days(pos_or_panic!(45.75));
473        let debug_string = format!("{expiration:?}");
474        assert_eq!(debug_string, "Days(45.75)");
475    }
476
477    #[test]
478    fn test_debug_datetime() {
479        let date = Utc.from_utc_datetime(
480            &NaiveDate::from_ymd_opt(2023, 12, 31)
481                .expect("Invalid date")
482                .and_hms_opt(23, 59, 59)
483                .expect("Invalid Time"),
484        );
485
486        let expiration = ExpirationDate::DateTime(date);
487        let debug_string = format!("{expiration:?}");
488        assert_eq!(debug_string, "DateTime(2023-12-31T23:59:59Z)");
489    }
490
491    #[test]
492    fn test_display_past_date() {
493        let past_date = Utc::now() - Duration::days(5);
494        let expiration = ExpirationDate::DateTime(past_date);
495        let display_string = format!("{expiration}");
496        assert!(display_string.contains("UTC"));
497        assert!(display_string.contains(&past_date.format("%Y-%m-%d %H:%M:%S").to_string()));
498    }
499
500    #[test]
501    fn test_display_today() {
502        let today = Utc::now();
503        let expiration = ExpirationDate::DateTime(today);
504        let display_string = format!("{expiration}");
505        info!("{}", display_string);
506        assert!(display_string.contains(&today.format("%Y-%m-%d %H:%M:%S").to_string()));
507    }
508}
509
510#[cfg(test)]
511mod tests_position_type_display_debug {
512    use super::*;
513    use crate::model::types::{OptionStyle, OptionType, Side};
514
515    use chrono::{DateTime, NaiveDate, TimeZone, Utc};
516    use expiration_date::ExpirationDate;
517    use positive::{Positive, pos_or_panic};
518    use rust_decimal_macros::dec;
519
520    fn get_option() -> (Options, DateTime<Utc>) {
521        let naive_date = NaiveDate::from_ymd_opt(2024, 8, 8)
522            .expect("Invalid date")
523            .and_hms_opt(0, 0, 0)
524            .expect("Invalid time");
525
526        (
527            Options {
528                option_type: OptionType::European,
529                side: Side::Long,
530                underlying_symbol: "AAPL".to_string(),
531                strike_price: pos_or_panic!(150.0),
532                expiration_date: ExpirationDate::DateTime(Utc.from_utc_datetime(&naive_date)),
533                implied_volatility: pos_or_panic!(0.25),
534                quantity: pos_or_panic!(10.0),
535                contract_size: Positive::ONE,
536                underlying_price: pos_or_panic!(155.0),
537                risk_free_rate: dec!(0.01),
538                option_style: OptionStyle::Call,
539                dividend_yield: pos_or_panic!(0.02),
540                exotic_params: None,
541            },
542            Utc.from_utc_datetime(&naive_date),
543        )
544    }
545
546    #[test]
547    fn test_position_display() {
548        let (option, naive_date) = get_option();
549        let position = Position {
550            option,
551            premium: pos_or_panic!(5.75),
552            date: naive_date,
553            open_fee: pos_or_panic!(0.50),
554            close_fee: pos_or_panic!(0.45),
555            epic: Some("Epic123".to_string()),
556            extra_fields: None,
557        };
558
559        let expected_display = "Position Details:\n\
560                Option: Long Call European Option\n\
561                Underlying: AAPL @ $155.00\n\
562                Strike: $150.00\n\
563                Expiration: 2024-08-08 00:00:00 UTC\n\
564                Implied Volatility: 25.00%\n\
565                Quantity: 10\n\
566                Risk-free Rate: 1.00%\n\
567                Dividend Yield: 2.00%\n\
568                Premium per contract: $5.75\n\
569                Date: 2024-08-08 00:00:00 UTC\n\
570                Open Fee per contract: $0.50\n\
571                Close Fee per contract: $0.45";
572
573        assert_eq!(format!("{position}"), expected_display);
574    }
575
576    #[test]
577    fn test_position_debug() {
578        let (option, naive_date) = get_option();
579
580        let position = Position {
581            option,
582            premium: pos_or_panic!(5.75),
583            date: naive_date,
584            open_fee: pos_or_panic!(0.50),
585            close_fee: pos_or_panic!(0.45),
586            epic: Some("Epic123".to_string()),
587            extra_fields: None,
588        };
589
590        let expected_debug = "Position { \
591        option: Options { \
592            option_type: European, \
593            side: Side::Long, \
594            underlying_symbol: \"AAPL\", \
595            strike_price: 150, \
596            expiration_date: DateTime(2024-08-08T00:00:00Z), \
597            implied_volatility: 0.25, \
598            quantity: 10, \
599            underlying_price: 155, \
600            risk_free_rate: 0.01, \
601            option_style: OptionStyle::Call, \
602            dividend_yield: 0.02, \
603            exotic_params: None \
604        }, \
605        premium: 5.75, \
606        date: 2024-08-08T00:00:00Z, \
607        open_fee: 0.5, \
608        close_fee: 0.45 \
609    }";
610
611        assert_eq!(format!("{position:?}"), expected_debug);
612    }
613}
614
615#[cfg(test)]
616mod tests_display_money {
617    use super::*;
618    use rust_decimal_macros::dec;
619
620    #[test]
621    fn test_display_money_finite_amount_keeps_precision() {
622        assert_eq!(format!("{:.2}", DisplayMoney(dec!(12.5))), "$12.50");
623        assert_eq!(format!("{:.2}", DisplayMoney(dec!(-3.25))), "$-3.25");
624        assert_eq!(DisplayMoney(dec!(7)).to_string(), "$7");
625        assert_eq!(
626            DisplayMoney::from(positive::pos_or_panic!(5.00)).to_string(),
627            "$5"
628        );
629    }
630
631    #[test]
632    fn test_display_money_decimal_max_renders_unlimited() {
633        assert_eq!(format!("{:.2}", DisplayMoney(Decimal::MAX)), "Unlimited");
634        assert_eq!(DisplayMoney::from(Positive::MAX).to_string(), "Unlimited");
635    }
636
637    #[test]
638    fn test_display_money_decimal_min_renders_negative_unlimited() {
639        assert_eq!(format!("{:.2}", DisplayMoney(Decimal::MIN)), "-Unlimited");
640    }
641
642    #[test]
643    fn test_unlimited_label_only_for_sentinels() {
644        assert_eq!(unlimited_label(Decimal::MAX), Some(UNLIMITED));
645        assert_eq!(unlimited_label(Decimal::MIN), Some("-Unlimited"));
646        assert_eq!(unlimited_label(Decimal::MAX - Decimal::ONE), None);
647        assert_eq!(unlimited_label(Decimal::ZERO), None);
648    }
649}