1use 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#[cw_serde]
19#[derive(Copy, Eq)]
20pub struct PricePoint {
21 pub price_notional: Price,
26 pub price_usd: PriceCollateralInUsd,
31 pub price_base: PriceBaseInQuote,
33 pub timestamp: Timestamp,
39 pub is_notional_usd: bool,
47 pub market_type: MarketType,
50 pub publish_time: Option<Timestamp>,
54 pub publish_time_usd: Option<Timestamp>,
58}
59
60impl PricePoint {
61 pub fn base_to_collateral(&self, base: Base) -> Collateral {
63 self.price_notional
64 .base_to_collateral(self.market_type, base)
65 }
66
67 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 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 pub fn collateral_to_usd(&self, collateral: Collateral) -> Usd {
81 self.price_usd.collateral_to_usd(collateral)
82 }
83
84 pub fn usd_to_collateral(&self, usd: Usd) -> Collateral {
86 self.price_usd.usd_to_collateral(usd)
87 }
88
89 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 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 pub fn notional_to_collateral(&self, amount: Notional) -> Collateral {
105 self.price_notional.notional_to_collateral(amount)
106 }
107
108 pub fn collateral_to_notional(&self, amount: Collateral) -> Notional {
110 self.price_notional.collateral_to_notional(amount)
111 }
112
113 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#[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 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 pub fn into_price_key(self, market_type: MarketType) -> PriceKey {
162 self.into_notional_price(market_type).into()
163 }
164
165 pub fn try_from_number(raw: Number) -> Result<Self> {
167 raw.try_into().map(PriceBaseInQuote)
168 }
169
170 pub fn into_number(&self) -> Number {
172 self.0.into()
173 }
174
175 pub fn into_non_zero(&self) -> NonZero<Decimal256> {
177 self.0
178 }
179
180 pub fn from_non_zero(raw: NonZero<Decimal256>) -> Self {
182 Self(raw)
183 }
184
185 pub fn try_into_usd(&self, market_id: &MarketId) -> Option<PriceCollateralInUsd> {
188 if market_id.get_base() == "USD" {
190 Some(match market_id.get_market_type() {
191 MarketType::CollateralIsQuote => {
192 PriceCollateralInUsd::from_non_zero(self.into_non_zero().inverse())
198 }
199 MarketType::CollateralIsBase => {
200 PriceCollateralInUsd::one()
204 }
205 })
206 } else if market_id.get_quote() == "USD" {
207 Some(match market_id.get_market_type() {
208 MarketType::CollateralIsQuote => {
209 PriceCollateralInUsd::one()
213 }
214 MarketType::CollateralIsBase => {
215 PriceCollateralInUsd::from_non_zero(self.into_non_zero())
221 }
222 })
223 } else {
224 None
226 }
227 }
228}
229
230#[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 pub fn try_from_number(raw: Number) -> Result<Self> {
253 raw.try_into().map(PriceCollateralInUsd)
254 }
255
256 pub fn from_non_zero(raw: NonZero<Decimal256>) -> Self {
258 PriceCollateralInUsd(raw)
259 }
260
261 pub fn one() -> Self {
263 Self(NonZero::one())
264 }
265
266 pub fn into_number(&self) -> Number {
268 self.0.into()
269 }
270
271 fn collateral_to_usd(&self, collateral: Collateral) -> Usd {
273 Usd::from_decimal256(collateral.into_decimal256() * self.0.raw())
274 }
275
276 fn usd_to_collateral(&self, usd: Usd) -> Collateral {
278 Collateral::from_decimal256(usd.into_decimal256() / self.0.raw())
279 }
280
281 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#[derive(
292 Debug,
293 Copy,
294 PartialOrd,
295 Ord,
296 Clone,
297 PartialEq,
298 Eq,
299 serde::Serialize,
300 serde::Deserialize,
301 JsonSchema,
305)]
306pub struct Price(NumberGtZero);
307
308impl Price {
309 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 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 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 fn notional_to_collateral(&self, amount: Notional) -> Collateral {
340 Collateral::from_decimal256(amount.into_decimal256() * self.0.raw())
341 }
342
343 fn collateral_to_notional(&self, amount: Collateral) -> Notional {
345 Notional::from_decimal256(amount.into_decimal256() / self.0.raw())
346 }
347
348 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 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 pub fn into_number(&self) -> Number {
383 self.0.into()
384 }
385
386 pub fn into_decimal256(self) -> Decimal256 {
388 self.0.raw()
389 }
390}
391
392#[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 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
491const POS_INF_STR: &str = "+Inf";
493
494#[derive(Debug, Clone, Copy, Eq, PartialEq)]
499#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
500pub enum TakeProfitTrader {
501 Finite(NonZero<Decimal256>),
503 PosInfinity,
505}
506
507impl TakeProfitTrader {
508 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 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 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}