levana_perpswap_cosmos/
price.rs

1//! Data types and conversion functions for different price representations.
2use schemars::{
3    schema::{InstanceType, SchemaObject},
4    JsonSchema,
5};
6use std::{fmt::Display, str::FromStr};
7
8use cosmwasm_schema::cw_serde;
9use cosmwasm_std::{Decimal256, StdError, StdResult};
10use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
11
12use crate::prelude::*;
13
14/// All prices in the protocol for a given point in time.
15///
16/// This includes extra information necessary for performing all conversions,
17/// such as the [MarketType].
18#[cw_serde]
19#[derive(Copy, Eq)]
20pub struct PricePoint {
21    /// Price as used internally by the protocol, in terms of collateral and notional.
22    ///
23    /// This is generally less useful for external consumers, where
24    /// [PricePoint::price_usd] and [PricePoint::price_base] are used.
25    pub price_notional: Price,
26    /// Price of the collateral asset in terms of USD.
27    ///
28    /// This is generally used for reporting of values like PnL and trade
29    /// volume.
30    pub price_usd: PriceCollateralInUsd,
31    /// Price of the base asset in terms of the quote.
32    pub price_base: PriceBaseInQuote,
33    /// Publish time of this price point.
34    ///
35    /// Before deferred execution, this was the block time when the field was
36    /// added. Since deferred execution, this is a calculated value based on the publish
37    /// times of individual feeds.
38    pub timestamp: Timestamp,
39    /// Is the notional asset USD?
40    ///
41    /// Used for avoiding lossy conversions to USD when they aren't needed.
42    ///
43    /// We do not need to track if the collateral asset is USD, since USD can
44    /// never be used as collateral directly. Instead, stablecoins would be
45    /// used, in which case an explicit price to USD is always needed.
46    pub is_notional_usd: bool,
47    /// Indicates if this market uses collateral as base or quote, needed for
48    /// price conversions.
49    pub market_type: MarketType,
50    /// Latest price publish time for the feeds composing the price, if available
51    ///
52    /// This field will always be empty since implementation of deferred execution.
53    pub publish_time: Option<Timestamp>,
54    /// Latest price publish time for the feeds composing the price_usd, if available
55    ///
56    /// This field will always be empty since implementation of deferred execution.
57    pub publish_time_usd: Option<Timestamp>,
58}
59
60impl PricePoint {
61    /// Convert a base value into collateral.
62    pub fn base_to_collateral(&self, base: Base) -> Collateral {
63        self.price_notional
64            .base_to_collateral(self.market_type, base)
65    }
66
67    /// Convert a base value into USD.
68    pub fn base_to_usd(&self, base: Base) -> Usd {
69        self.price_usd
70            .collateral_to_usd(self.base_to_collateral(base))
71    }
72
73    /// Convert a non-zero collateral value into base.
74    pub fn collateral_to_base_non_zero(&self, collateral: NonZero<Collateral>) -> NonZero<Base> {
75        self.price_notional
76            .collateral_to_base_non_zero(self.market_type, collateral)
77    }
78
79    /// Convert a collateral value into USD.
80    pub fn collateral_to_usd(&self, collateral: Collateral) -> Usd {
81        self.price_usd.collateral_to_usd(collateral)
82    }
83
84    /// Convert a USD value into collateral.
85    pub fn usd_to_collateral(&self, usd: Usd) -> Collateral {
86        self.price_usd.usd_to_collateral(usd)
87    }
88
89    /// Keeps the invariant of a non-zero value
90    pub fn collateral_to_usd_non_zero(&self, collateral: NonZero<Collateral>) -> NonZero<Usd> {
91        self.price_usd.collateral_to_usd_non_zero(collateral)
92    }
93
94    /// Convert a notional value into USD.
95    pub fn notional_to_usd(&self, notional: Notional) -> Usd {
96        if self.is_notional_usd {
97            Usd::from_decimal256(notional.into_decimal256())
98        } else {
99            self.collateral_to_usd(self.notional_to_collateral(notional))
100        }
101    }
102
103    /// Convert an amount in notional into an amount in collateral
104    pub fn notional_to_collateral(&self, amount: Notional) -> Collateral {
105        self.price_notional.notional_to_collateral(amount)
106    }
107
108    /// Convert an amount in collateral into an amount in notional
109    pub fn collateral_to_notional(&self, amount: Collateral) -> Notional {
110        self.price_notional.collateral_to_notional(amount)
111    }
112
113    /// Convert a non-zero amount in collateral into a non-zero amount in notional
114    pub fn collateral_to_notional_non_zero(
115        &self,
116        amount: NonZero<Collateral>,
117    ) -> NonZero<Notional> {
118        NonZero::new(self.collateral_to_notional(amount.raw()))
119            .expect("collateral_to_notional_non_zero: impossible 0 result")
120    }
121}
122
123/// The price of the currency pair, given as `quote / base`, e.g. "20,000 USD per BTC".
124#[cw_serde]
125#[derive(Copy, Eq)]
126#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
127pub struct PriceBaseInQuote(NumberGtZero);
128
129impl Display for PriceBaseInQuote {
130    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
131        self.0.fmt(f)
132    }
133}
134
135impl FromStr for PriceBaseInQuote {
136    type Err = anyhow::Error;
137
138    fn from_str(s: &str) -> Result<Self, Self::Err> {
139        s.parse().map(PriceBaseInQuote)
140    }
141}
142
143impl TryFrom<&str> for PriceBaseInQuote {
144    type Error = anyhow::Error;
145
146    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
147        PriceBaseInQuote::from_str(value)
148    }
149}
150
151impl PriceBaseInQuote {
152    /// Convert to the internal price representation used by our system, as `collateral / notional`.
153    pub fn into_notional_price(self, market_type: MarketType) -> Price {
154        Price(match market_type {
155            MarketType::CollateralIsQuote => self.0,
156            MarketType::CollateralIsBase => self.0.inverse(),
157        })
158    }
159
160    /// Convert into a [PriceKey] representation.
161    pub fn into_price_key(self, market_type: MarketType) -> PriceKey {
162        self.into_notional_price(market_type).into()
163    }
164
165    /// Try to convert a signed decimal into a price.
166    pub fn try_from_number(raw: Number) -> Result<Self> {
167        raw.try_into().map(PriceBaseInQuote)
168    }
169
170    /// Convert into a signed decimal.
171    pub fn into_number(&self) -> Number {
172        self.0.into()
173    }
174
175    /// Convert into a non-zero decimal.
176    pub fn into_non_zero(&self) -> NonZero<Decimal256> {
177        self.0
178    }
179
180    /// Convert from a non-zero decimal.
181    pub fn from_non_zero(raw: NonZero<Decimal256>) -> Self {
182        Self(raw)
183    }
184
185    /// Derive the USD price from base and market type.
186    /// This is only possible when one of the assets is USD.
187    pub fn try_into_usd(&self, market_id: &MarketId) -> Option<PriceCollateralInUsd> {
188        // For comments below, assume we're dealing with a pair between USD and ATOM
189        if market_id.get_base() == "USD" {
190            Some(match market_id.get_market_type() {
191                MarketType::CollateralIsQuote => {
192                    // Base == USD, quote == collateral == ATOM
193                    // price = ATOM/USD
194                    // Return value = USD/ATOM
195                    //
196                    // Therefore, we need to invert the numbers
197                    PriceCollateralInUsd::from_non_zero(self.into_non_zero().inverse())
198                }
199                MarketType::CollateralIsBase => {
200                    // Base == collateral == USD
201                    // Return value == USD/USD
202                    // QED it's one
203                    PriceCollateralInUsd::one()
204                }
205            })
206        } else if market_id.get_quote() == "USD" {
207            Some(match market_id.get_market_type() {
208                MarketType::CollateralIsQuote => {
209                    // Collateral == quote == USD
210                    // Return value = USD/USD
211                    // QED it's one
212                    PriceCollateralInUsd::one()
213                }
214                MarketType::CollateralIsBase => {
215                    // Collateral == base == ATOM
216                    // Quote == USD
217                    // Price = USD/ATOM
218                    // Return value = USD/ATOM
219                    // QED same number
220                    PriceCollateralInUsd::from_non_zero(self.into_non_zero())
221                }
222            })
223        } else {
224            // Neither asset is USD, so we can't get a price
225            None
226        }
227    }
228}
229
230/// PriceBaseInQuote converted to USD
231#[cw_serde]
232#[derive(Copy, Eq)]
233#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
234pub struct PriceCollateralInUsd(NumberGtZero);
235
236impl Display for PriceCollateralInUsd {
237    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
238        self.0.fmt(f)
239    }
240}
241
242impl FromStr for PriceCollateralInUsd {
243    type Err = anyhow::Error;
244
245    fn from_str(s: &str) -> Result<Self, Self::Err> {
246        s.parse().map(PriceCollateralInUsd)
247    }
248}
249
250impl PriceCollateralInUsd {
251    /// Try to convert from a signed decimal.
252    pub fn try_from_number(raw: Number) -> Result<Self> {
253        raw.try_into().map(PriceCollateralInUsd)
254    }
255
256    /// Convert from a non-zero decimal.
257    pub fn from_non_zero(raw: NonZero<Decimal256>) -> Self {
258        PriceCollateralInUsd(raw)
259    }
260
261    /// The price point of 1
262    pub fn one() -> Self {
263        Self(NonZero::one())
264    }
265
266    /// Convert into a signed decimal
267    pub fn into_number(&self) -> Number {
268        self.0.into()
269    }
270
271    /// Convert a collateral value into USD.
272    fn collateral_to_usd(&self, collateral: Collateral) -> Usd {
273        Usd::from_decimal256(collateral.into_decimal256() * self.0.raw())
274    }
275
276    /// Convert a USD value into collateral.
277    fn usd_to_collateral(&self, usd: Usd) -> Collateral {
278        Collateral::from_decimal256(usd.into_decimal256() / self.0.raw())
279    }
280
281    /// Keeps the invariant of a non-zero value
282    fn collateral_to_usd_non_zero(&self, collateral: NonZero<Collateral>) -> NonZero<Usd> {
283        NonZero::new(Usd::from_decimal256(
284            collateral.into_decimal256() * self.0.raw(),
285        ))
286        .expect("collateral_to_usd_non_zero: Impossible! Output cannot be 0")
287    }
288}
289
290/// The price of the pair as used internally by the protocol, given as `collateral / notional`.
291#[derive(
292    Debug,
293    Copy,
294    PartialOrd,
295    Ord,
296    Clone,
297    PartialEq,
298    Eq,
299    serde::Serialize,
300    serde::Deserialize,
301    // It would be better not to have this impl to ensure we never send protocol
302    // prices over the wire, but that will break other parts of the API. May want to
303    // come back to that later.
304    JsonSchema,
305)]
306pub struct Price(NumberGtZero);
307
308impl Price {
309    /// Convert to the external representation.
310    pub fn into_base_price(self, market_type: MarketType) -> PriceBaseInQuote {
311        PriceBaseInQuote(match market_type {
312            MarketType::CollateralIsQuote => self.0,
313            MarketType::CollateralIsBase => self.0.inverse(),
314        })
315    }
316
317    /// Convert a non-zero amount in collateral into an amount in base
318    fn collateral_to_base_non_zero(
319        &self,
320        market_type: MarketType,
321        collateral: NonZero<Collateral>,
322    ) -> NonZero<Base> {
323        NonZero::new(Base::from_decimal256(match market_type {
324            MarketType::CollateralIsQuote => collateral.into_decimal256() / self.0.raw(),
325            MarketType::CollateralIsBase => collateral.into_decimal256(),
326        }))
327        .expect("collateral_to_base_non_zero: impossible 0 value as a result")
328    }
329
330    /// Convert an amount in base into an amount in collateral
331    fn base_to_collateral(&self, market_type: MarketType, amount: Base) -> Collateral {
332        Collateral::from_decimal256(match market_type {
333            MarketType::CollateralIsQuote => amount.into_decimal256() * self.0.raw(),
334            MarketType::CollateralIsBase => amount.into_decimal256(),
335        })
336    }
337
338    /// Convert an amount in notional into an amount in collateral
339    fn notional_to_collateral(&self, amount: Notional) -> Collateral {
340        Collateral::from_decimal256(amount.into_decimal256() * self.0.raw())
341    }
342
343    /// Convert an amount in collateral into an amount in notional, but with types
344    fn collateral_to_notional(&self, amount: Collateral) -> Notional {
345        Notional::from_decimal256(amount.into_decimal256() / self.0.raw())
346    }
347
348    /// Convert an amount in collateral into an amount in notional, but with types
349    pub fn collateral_to_notional_non_zero(
350        &self,
351        amount: NonZero<Collateral>,
352    ) -> NonZero<Notional> {
353        NonZero::new(Notional::from_decimal256(
354            amount.into_decimal256() / self.0.raw(),
355        ))
356        .expect("collateral_to_notional_non_zero resulted in 0")
357    }
358}
359
360impl Display for Price {
361    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
362        self.into_number().fmt(f)
363    }
364}
365
366impl Price {
367    /// Attempt to convert a [Number] into a price.
368    ///
369    /// This will fail on zero or negative numbers. Callers need to ensure that
370    /// the incoming [Number] is a protocol price, not a [PriceBaseInQuote].
371    pub fn try_from_number(number: Number) -> Result<Price> {
372        number
373            .try_into()
374            .map(Price)
375            .context("Cannot convert number to Price")
376    }
377
378    /// Convert to a raw [Number].
379    ///
380    /// Note that in the future we may want to hide this functionality to force
381    /// usage of well-typed interfaces here.
382    pub fn into_number(&self) -> Number {
383        self.0.into()
384    }
385
386    /// Convert to a raw [Decimal256].
387    pub fn into_decimal256(self) -> Decimal256 {
388        self.0.raw()
389    }
390}
391
392/// A modified version of a [Price] used as a key in a `Map`.
393///
394/// Due to how cw-storage-plus works, we need to have a reference to a slice,
395/// which we can't get from a `Decimal256`. Instead, we store an array directly
396/// here and provide conversion functions.
397#[derive(Clone)]
398pub struct PriceKey([u8; 32]);
399
400impl<'a> PrimaryKey<'a> for PriceKey {
401    type Prefix = ();
402    type SubPrefix = ();
403    type Suffix = Self;
404    type SuperSuffix = Self;
405
406    fn key(&self) -> Vec<Key> {
407        vec![Key::Ref(&self.0)]
408    }
409}
410
411impl<'a> Prefixer<'a> for PriceKey {
412    fn prefix(&self) -> Vec<Key> {
413        self.key()
414    }
415}
416
417impl KeyDeserialize for PriceKey {
418    type Output = Price;
419
420    const KEY_ELEMS: u16 = 1;
421
422    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
423        value
424            .try_into()
425            .ok()
426            .and_then(|bytes| NumberGtZero::from_be_bytes(bytes).map(Price))
427            .ok_or_else(|| StdError::generic_err("unable to convert value into Price"))
428    }
429}
430
431impl From<Price> for PriceKey {
432    fn from(price: Price) -> Self {
433        PriceKey(price.0.to_be_bytes())
434    }
435}
436
437impl TryFrom<pyth_sdk_cw::Price> for Number {
438    type Error = anyhow::Error;
439    fn try_from(price: pyth_sdk_cw::Price) -> Result<Self, Self::Error> {
440        let n: Number = price.price.to_string().parse()?;
441
442        match price.expo.cmp(&0) {
443            std::cmp::Ordering::Equal => Ok(n),
444            std::cmp::Ordering::Greater => n * Number::from(10u128.pow(price.expo.unsigned_abs())),
445            std::cmp::Ordering::Less => n / Number::from(10u128.pow(price.expo.unsigned_abs())),
446        }
447    }
448}
449
450impl Number {
451    /// Converts a Number into a pyth price
452    /// the exponent will always be 0 or negative
453    pub fn to_pyth_price(
454        &self,
455        conf: u64,
456        publish_time: pyth_sdk_cw::UnixTimestamp,
457    ) -> Result<pyth_sdk_cw::Price> {
458        let s = self.to_string();
459        let (integer, decimal) = s.split_once('.').unwrap_or((&s, ""));
460        let price: i64 = format!("{}{}", integer, decimal).parse()?;
461        let mut expo: i32 = decimal.len().try_into()?;
462        if expo > 0 {
463            expo = -expo;
464        }
465
466        Ok(pyth_sdk_cw::Price {
467            price,
468            expo,
469            conf,
470            publish_time,
471        })
472    }
473}
474
475impl TryFrom<pyth_sdk_cw::Price> for PriceBaseInQuote {
476    type Error = anyhow::Error;
477
478    fn try_from(value: pyth_sdk_cw::Price) -> Result<Self, Self::Error> {
479        Self::try_from_number(value.try_into()?)
480    }
481}
482
483impl TryFrom<pyth_sdk_cw::Price> for PriceCollateralInUsd {
484    type Error = anyhow::Error;
485
486    fn try_from(value: pyth_sdk_cw::Price) -> Result<Self, Self::Error> {
487        Self::try_from_number(value.try_into()?)
488    }
489}
490
491/// String representation of positive infinity.
492const POS_INF_STR: &str = "+Inf";
493
494/// The take profit price for a position, as supplied by client messsages (in terms of BaseInQuote).
495///
496/// Infinite take profit price is possible. However, this is an error in the case of
497/// short positions or collateral-is-quote markets.
498#[derive(Debug, Clone, Copy, Eq, PartialEq)]
499#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
500pub enum TakeProfitTrader {
501    /// Finite take profit price
502    Finite(NonZero<Decimal256>),
503    /// Infinite take profit price
504    PosInfinity,
505}
506
507impl TakeProfitTrader {
508    /// helper to extract the inner value if it is finite
509    pub fn as_finite(&self) -> Option<NonZero<Decimal256>> {
510        match self {
511            TakeProfitTrader::Finite(val) => Some(*val),
512            TakeProfitTrader::PosInfinity => None,
513        }
514    }
515
516    /// Convert to the internal price representation used by our system, as `collateral / notional`.
517    pub fn into_notional(&self, market_type: MarketType) -> Option<Price> {
518        match self {
519            TakeProfitTrader::PosInfinity => None,
520            TakeProfitTrader::Finite(x) => {
521                Some(PriceBaseInQuote::from_non_zero(*x).into_notional_price(market_type))
522            }
523        }
524    }
525}
526
527impl Display for TakeProfitTrader {
528    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
529        match self {
530            TakeProfitTrader::Finite(val) => val.fmt(f),
531            TakeProfitTrader::PosInfinity => write!(f, "{}", POS_INF_STR),
532        }
533    }
534}
535
536impl FromStr for TakeProfitTrader {
537    type Err = PerpError;
538    fn from_str(src: &str) -> Result<Self, PerpError> {
539        match src {
540            POS_INF_STR => Ok(TakeProfitTrader::PosInfinity),
541            _ => match src.parse() {
542                Ok(number) => Ok(TakeProfitTrader::Finite(number)),
543                Err(err) => Err(PerpError::new(
544                    ErrorId::Conversion,
545                    ErrorDomain::Default,
546                    format!("error converting {} to TakeProfitPrice , {}", src, err),
547                )),
548            },
549        }
550    }
551}
552
553impl TryFrom<&str> for TakeProfitTrader {
554    type Error = anyhow::Error;
555
556    fn try_from(val: &str) -> Result<Self, Self::Error> {
557        Self::from_str(val).map_err(|err| err.into())
558    }
559}
560
561impl From<PriceBaseInQuote> for TakeProfitTrader {
562    fn from(val: PriceBaseInQuote) -> Self {
563        TakeProfitTrader::Finite(val.into_non_zero())
564    }
565}
566
567impl serde::Serialize for TakeProfitTrader {
568    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
569    where
570        S: serde::Serializer,
571    {
572        match self {
573            TakeProfitTrader::Finite(number) => number.serialize(serializer),
574            TakeProfitTrader::PosInfinity => serializer.serialize_str(POS_INF_STR),
575        }
576    }
577}
578
579impl<'de> serde::Deserialize<'de> for TakeProfitTrader {
580    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
581    where
582        D: serde::Deserializer<'de>,
583    {
584        deserializer.deserialize_str(TakeProfitPriceVisitor)
585    }
586}
587
588impl JsonSchema for TakeProfitTrader {
589    fn schema_name() -> String {
590        "TakeProfitPrice".to_owned()
591    }
592
593    fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
594        SchemaObject {
595            instance_type: Some(InstanceType::String.into()),
596            format: Some("take-profit".to_owned()),
597            ..Default::default()
598        }
599        .into()
600    }
601}
602
603struct TakeProfitPriceVisitor;
604
605impl<'de> serde::de::Visitor<'de> for TakeProfitPriceVisitor {
606    type Value = TakeProfitTrader;
607
608    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
609        formatter.write_str("TakeProfitPrice")
610    }
611
612    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
613    where
614        E: serde::de::Error,
615    {
616        v.parse()
617            .map_err(|_| E::custom(format!("Invalid TakeProfitPrice: {v}")))
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    #[test]
626    fn price_parse() {
627        PriceBaseInQuote::from_str("1").unwrap();
628        PriceBaseInQuote::from_str("1.0").unwrap();
629        PriceBaseInQuote::from_str("1..0").unwrap_err();
630        PriceBaseInQuote::from_str(".1").unwrap_err();
631        PriceBaseInQuote::from_str("0.1").unwrap();
632        PriceBaseInQuote::from_str("-0.1").unwrap_err();
633        PriceBaseInQuote::from_str("-0.0").unwrap_err();
634        PriceBaseInQuote::from_str("-0").unwrap_err();
635        PriceBaseInQuote::from_str("0").unwrap_err();
636        PriceBaseInQuote::from_str("0.0").unwrap_err();
637        PriceBaseInQuote::from_str("0.001").unwrap();
638        PriceBaseInQuote::from_str("0.00100").unwrap();
639    }
640
641    #[test]
642    fn deserialize_price() {
643        let go = serde_json::from_str::<PriceBaseInQuote>;
644
645        go("\"1.0\"").unwrap();
646        go("\"1.1\"").unwrap();
647        go("\"-1.1\"").unwrap_err();
648        go("\"-0\"").unwrap_err();
649        go("\"0\"").unwrap_err();
650        go("\"0.1\"").unwrap();
651    }
652
653    #[test]
654    fn pyth_price() {
655        let go = |price: i64, expo: i32, expected: &str| {
656            let pyth_price = pyth_sdk_cw::Price {
657                price,
658                expo,
659                conf: 0,
660                publish_time: 0,
661            };
662            let n = Number::from_str(expected).unwrap();
663            assert_eq!(Number::try_from(pyth_price).unwrap(), n);
664
665            // number-to-pyth-price only uses the `expo` field to add decimal places
666            // so we need to compare to the expected string via round-tripping
667            // which we can do since the above test already confirms the conversion is correct
668            assert_eq!(
669                Number::try_from(
670                    n.to_pyth_price(pyth_price.conf, pyth_price.publish_time)
671                        .unwrap()
672                )
673                .unwrap(),
674                n
675            );
676            if price > 0 {
677                assert_eq!(
678                    PriceBaseInQuote::try_from(pyth_price).unwrap(),
679                    PriceBaseInQuote::from_str(expected).unwrap()
680                );
681                assert_eq!(
682                    PriceCollateralInUsd::try_from(pyth_price).unwrap(),
683                    PriceCollateralInUsd::from_str(expected).unwrap()
684                );
685            }
686        };
687
688        go(123456789, 0, "123456789.0");
689        go(-123456789, 0, "-123456789.0");
690        go(123456789, -3, "123456.789");
691        go(123456789, 3, "123456789000.0");
692        go(-123456789, -3, "-123456.789");
693        go(-123456789, 3, "-123456789000.0");
694        go(12345600789, -5, "123456.00789");
695        go(1234560078900, -7, "123456.00789");
696    }
697
698    #[test]
699    fn take_profit_price() {
700        fn go(s: &str, expected: TakeProfitTrader) {
701            let deserialized = serde_json::from_str::<TakeProfitTrader>(s).unwrap();
702            assert_eq!(deserialized, expected);
703            let serialized = serde_json::to_string(&expected).unwrap();
704            assert_eq!(serialized, s);
705        }
706
707        go("\"1.2\"", TakeProfitTrader::Finite("1.2".parse().unwrap()));
708        go("\"+Inf\"", TakeProfitTrader::PosInfinity);
709    }
710}