gmsol_model/action/
swap.rs

1use std::fmt;
2
3use crate::{
4    market::{BaseMarket, BaseMarketExt},
5    num::{MulDiv, Unsigned, UnsignedAbs},
6    params::Fees,
7    price::{Price, Prices},
8    BalanceExt, Delta, PnlFactorKind, Pool, SwapMarketExt, SwapMarketMut,
9};
10
11use num_traits::{CheckedAdd, CheckedMul, CheckedNeg, CheckedSub, Signed, Zero};
12
13use super::MarketAction;
14
15/// A swap.
16#[must_use = "actions do nothing unless you `execute` them"]
17pub struct Swap<M: BaseMarket<DECIMALS>, const DECIMALS: u8> {
18    market: M,
19    params: SwapParams<M::Num>,
20}
21
22impl<const DECIMALS: u8, M: SwapMarketMut<DECIMALS>> Swap<M, DECIMALS> {
23    /// Create a new swap in the given market.
24    pub fn try_new(
25        market: M,
26        is_token_in_long: bool,
27        token_in_amount: M::Num,
28        prices: Prices<M::Num>,
29    ) -> crate::Result<Self> {
30        if token_in_amount.is_zero() {
31            return Err(crate::Error::EmptySwap);
32        }
33        prices.validate()?;
34        Ok(Self {
35            market,
36            params: SwapParams {
37                is_token_in_long,
38                token_in_amount,
39                prices,
40            },
41        })
42    }
43
44    /// Assign the amounts of `token_in` and `token_out` to `long_token` and `short_token`, respectively,
45    /// and assign the prices of `long_token` and `short_token` to `token_in` and `token_out`.
46    fn reassign_values(&self) -> crate::Result<ReassignedValues<M::Num>> {
47        if self.params.is_token_in_long {
48            let long_delta_value: M::Signed = self
49                .params
50                .token_in_amount
51                .checked_mul(&self.params.long_token_price().mid())
52                .ok_or(crate::Error::Computation("long delta value"))?
53                .try_into()
54                .map_err(|_| crate::Error::Convert)?;
55            Ok(ReassignedValues::new(
56                long_delta_value.clone(),
57                long_delta_value
58                    .checked_neg()
59                    .ok_or(crate::Error::Computation("negating long delta value"))?,
60                self.params.long_token_price().clone(),
61                self.params.short_token_price().clone(),
62                PnlFactorKind::MaxAfterDeposit,
63                PnlFactorKind::MaxAfterWithdrawal,
64            ))
65        } else {
66            let short_delta_value: M::Signed = self
67                .params
68                .token_in_amount
69                .checked_mul(&self.params.short_token_price().mid())
70                .ok_or(crate::Error::Computation("short delta value"))?
71                .try_into()
72                .map_err(|_| crate::Error::Convert)?;
73            Ok(ReassignedValues::new(
74                short_delta_value
75                    .checked_neg()
76                    .ok_or(crate::Error::Computation("negating short delta value"))?,
77                short_delta_value,
78                self.params.short_token_price().clone(),
79                self.params.long_token_price().clone(),
80                PnlFactorKind::MaxAfterWithdrawal,
81                PnlFactorKind::MaxAfterDeposit,
82            ))
83        }
84    }
85
86    fn charge_fees(&self, is_positive_impact: bool) -> crate::Result<(M::Num, Fees<M::Num>)> {
87        self.market
88            .swap_fee_params()?
89            .apply_fees(is_positive_impact, &self.params.token_in_amount)
90            .ok_or(crate::Error::Computation("apply fees"))
91    }
92
93    #[allow(clippy::type_complexity)]
94    fn try_execute(
95        &self,
96    ) -> crate::Result<(
97        Cache<'_, M, DECIMALS>,
98        SwapResult<M::Num, <M::Num as Unsigned>::Signed>,
99    )> {
100        let ReassignedValues {
101            long_token_delta_value,
102            short_token_delta_value,
103            token_in_price,
104            token_out_price,
105            long_pnl_factor_kind,
106            short_pnl_factor_kind,
107        } = self.reassign_values()?;
108
109        // Calculate price impact.
110        //
111        // Note that the virtual inventory feature is not currently supported.
112        let price_impact = self
113            .market
114            .liquidity_pool()?
115            .pool_delta_with_values(
116                long_token_delta_value,
117                short_token_delta_value,
118                &self.params.long_token_price().mid(),
119                &self.params.short_token_price().mid(),
120            )?
121            .price_impact(&self.market.swap_impact_params()?)?;
122
123        let (amount_after_fees, fees) = self.charge_fees(price_impact.is_positive())?;
124
125        let claimable_fee =
126            self.market
127                .claimable_fee_pool()?
128                .checked_apply_delta(Delta::new_one_side(
129                    self.params.is_token_in_long,
130                    &fees.fee_amount_for_receiver().to_signed()?,
131                ))?;
132
133        // Calculate final amounts && apply delta to price impact pool.
134        let mut token_in_amount;
135        let token_out_amount;
136        let pool_amount_out;
137        let price_impact_amount;
138        let swap_impact;
139        if price_impact.is_positive() {
140            token_in_amount = amount_after_fees;
141
142            let swap_impact_deduct_side = !self.params.is_token_in_long;
143            let (signed_price_impact_amount, capped_diff_value) =
144                self.market.swap_impact_amount_with_cap(
145                    swap_impact_deduct_side,
146                    &token_out_price,
147                    &price_impact,
148                )?;
149            debug_assert!(!signed_price_impact_amount.is_negative());
150
151            let capped_diff_token_in_amount = if capped_diff_value.is_zero() {
152                Zero::zero()
153            } else {
154                // If the positive price impact was capped, use the token_in swap
155                // impact pool to pay for the positive price impact.
156                let (capped_diff_token_in_amount, _) = self.market.swap_impact_amount_with_cap(
157                    self.params.is_token_in_long,
158                    &token_in_price,
159                    &capped_diff_value.to_signed()?,
160                )?;
161                debug_assert!(!capped_diff_token_in_amount.is_negative());
162                token_in_amount = token_in_amount
163                    .checked_add(&capped_diff_token_in_amount.unsigned_abs())
164                    .ok_or(crate::Error::Computation("swap: adding capped diff amount"))?;
165                capped_diff_token_in_amount
166            };
167
168            swap_impact =
169                self.market
170                    .swap_impact_pool()?
171                    .checked_apply_delta(Delta::new_both_sides(
172                        swap_impact_deduct_side,
173                        &signed_price_impact_amount.checked_neg().ok_or(
174                            crate::Error::Computation("negating positive price impact amount "),
175                        )?,
176                        &capped_diff_token_in_amount
177                            .checked_neg()
178                            .ok_or(crate::Error::Computation("negating capped diff amount "))?,
179                    ))?;
180            price_impact_amount = signed_price_impact_amount.unsigned_abs();
181
182            pool_amount_out = token_in_amount
183                .checked_mul_div(
184                    token_in_price.pick_price(false),
185                    token_out_price.pick_price(true),
186                )
187                .ok_or(crate::Error::Computation(
188                    "pool amount out for positive impact",
189                ))?;
190            // Extra amount is deducted from the swap impact pool.
191            token_out_amount = pool_amount_out.checked_add(&price_impact_amount).ok_or(
192                crate::Error::Computation("token out amount for positive impact"),
193            )?;
194        } else {
195            let swap_impact_deduct_side = self.params.is_token_in_long;
196            let (signed_price_impact_amount, _) = self.market.swap_impact_amount_with_cap(
197                swap_impact_deduct_side,
198                &token_in_price,
199                &price_impact,
200            )?;
201            debug_assert!(!signed_price_impact_amount.is_positive());
202            swap_impact =
203                self.market
204                    .swap_impact_pool()?
205                    .checked_apply_delta(Delta::new_one_side(
206                        swap_impact_deduct_side,
207                        &signed_price_impact_amount.checked_neg().ok_or(
208                            crate::Error::Computation("negating negative price impact amount "),
209                        )?,
210                    ))?;
211            price_impact_amount = signed_price_impact_amount.unsigned_abs();
212
213            token_in_amount = amount_after_fees.checked_sub(&price_impact_amount).ok_or(
214                crate::Error::Computation("swap: not enough fund to pay price impact"),
215            )?;
216
217            if token_in_amount.is_zero() {
218                return Err(crate::Error::Computation(
219                    "swap: not enough fund to pay price impact",
220                ));
221            }
222
223            token_out_amount = token_in_amount
224                .checked_mul_div(
225                    token_in_price.pick_price(false),
226                    token_out_price.pick_price(true),
227                )
228                .ok_or(crate::Error::Computation(
229                    "token out amount for negative impact",
230                ))?;
231            pool_amount_out = token_out_amount.clone();
232        }
233
234        // Apply delta to liquidity pools.
235        // `token_in_amount` is assumed to have been transferred in.
236        let liquidity =
237            self.market
238                .liquidity_pool()?
239                .checked_apply_delta(Delta::new_both_sides(
240                    self.params.is_token_in_long,
241                    &token_in_amount
242                        .checked_add(fees.fee_amount_for_pool())
243                        .ok_or(crate::Error::Overflow)?
244                        .to_signed()?,
245                    &pool_amount_out.to_opposite_signed()?,
246                ))?;
247
248        let cache = Cache {
249            market: &self.market,
250            liquidity,
251            swap_impact,
252            claimable_fee,
253        };
254
255        cache.validate_pool_amount(self.params.is_token_in_long)?;
256        cache.validate_reserve(&self.params.prices, !self.params.is_token_in_long)?;
257        cache.validate_max_pnl(
258            &self.params.prices,
259            long_pnl_factor_kind,
260            short_pnl_factor_kind,
261        )?;
262
263        let result = SwapResult {
264            price_impact_value: price_impact,
265            token_in_fees: fees,
266            token_out_amount,
267            price_impact_amount,
268        };
269
270        Ok((cache, result))
271    }
272}
273
274impl<const DECIMALS: u8, M> MarketAction for Swap<M, DECIMALS>
275where
276    M: SwapMarketMut<DECIMALS>,
277{
278    type Report = SwapReport<M::Num, <M::Num as Unsigned>::Signed>;
279
280    /// Execute the swap.
281    /// # Notes
282    /// - This function is atomic.
283    fn execute(mut self) -> crate::Result<Self::Report> {
284        let (cache, result) = self.try_execute()?;
285
286        let Cache {
287            liquidity,
288            swap_impact,
289            claimable_fee,
290            ..
291        } = cache;
292
293        *self
294            .market
295            .liquidity_pool_mut()
296            .expect("liquidity pool must be valid") = liquidity;
297
298        *self
299            .market
300            .swap_impact_pool_mut()
301            .expect("swap impact pool must be valid") = swap_impact;
302
303        *self
304            .market
305            .claimable_fee_pool_mut()
306            .expect("claimable fee pool must be valid") = claimable_fee;
307
308        Ok(SwapReport {
309            params: self.params,
310            result,
311        })
312    }
313}
314
315/// Swap params.
316#[derive(Debug, Clone, Copy)]
317#[cfg_attr(
318    feature = "anchor-lang",
319    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
320)]
321pub struct SwapParams<T> {
322    is_token_in_long: bool,
323    token_in_amount: T,
324    prices: Prices<T>,
325}
326
327#[cfg(feature = "gmsol-utils")]
328impl<T: gmsol_utils::InitSpace> gmsol_utils::InitSpace for SwapParams<T> {
329    const INIT_SPACE: usize = bool::INIT_SPACE + T::INIT_SPACE + Prices::<T>::INIT_SPACE;
330}
331
332impl<T> SwapParams<T> {
333    /// Get long token price.
334    pub fn long_token_price(&self) -> &Price<T> {
335        &self.prices.long_token_price
336    }
337
338    /// Get short token price.
339    pub fn short_token_price(&self) -> &Price<T> {
340        &self.prices.short_token_price
341    }
342
343    /// Whether the in token is long token.
344    pub fn is_token_in_long(&self) -> bool {
345        self.is_token_in_long
346    }
347
348    /// Get the amount of in token.
349    pub fn token_in_amount(&self) -> &T {
350        &self.token_in_amount
351    }
352}
353
354#[derive(Debug, Clone, Copy)]
355#[cfg_attr(
356    feature = "anchor-lang",
357    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
358)]
359struct SwapResult<Unsigned, Signed> {
360    token_in_fees: Fees<Unsigned>,
361    token_out_amount: Unsigned,
362    price_impact_value: Signed,
363    price_impact_amount: Unsigned,
364}
365
366#[cfg(feature = "gmsol-utils")]
367impl<Unsigned, Signed> gmsol_utils::InitSpace for SwapResult<Unsigned, Signed>
368where
369    Unsigned: gmsol_utils::InitSpace,
370    Signed: gmsol_utils::InitSpace,
371{
372    const INIT_SPACE: usize = Fees::<Unsigned>::INIT_SPACE
373        + Unsigned::INIT_SPACE
374        + Signed::INIT_SPACE
375        + Unsigned::INIT_SPACE;
376}
377
378/// Report of the execution of swap.
379#[must_use = "`token_out_amount` must be used"]
380#[cfg_attr(
381    feature = "anchor-lang",
382    derive(anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)
383)]
384#[derive(Clone)]
385pub struct SwapReport<Unsigned, Signed> {
386    params: SwapParams<Unsigned>,
387    result: SwapResult<Unsigned, Signed>,
388}
389
390#[cfg(feature = "gmsol-utils")]
391impl<Unsigned, Signed> gmsol_utils::InitSpace for SwapReport<Unsigned, Signed>
392where
393    Unsigned: gmsol_utils::InitSpace,
394    Signed: gmsol_utils::InitSpace,
395{
396    const INIT_SPACE: usize =
397        SwapParams::<Unsigned>::INIT_SPACE + SwapResult::<Unsigned, Signed>::INIT_SPACE;
398}
399
400impl<T> fmt::Debug for SwapReport<T, T::Signed>
401where
402    T: Unsigned,
403    T: fmt::Debug,
404    T::Signed: fmt::Debug,
405{
406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407        f.debug_struct("SwapReport")
408            .field("params", &self.params)
409            .field("result", &self.result)
410            .finish()
411    }
412}
413
414impl<T: Unsigned> SwapReport<T, T::Signed> {
415    /// Get swap params.
416    pub fn params(&self) -> &SwapParams<T> {
417        &self.params
418    }
419
420    /// Get token in fees.
421    pub fn token_in_fees(&self) -> &Fees<T> {
422        &self.result.token_in_fees
423    }
424
425    /// Get the amount of out token.
426    #[must_use = "the returned amount of tokens should be transferred out from the market vault"]
427    pub fn token_out_amount(&self) -> &T {
428        &self.result.token_out_amount
429    }
430
431    /// Get the price impact for the swap.
432    pub fn price_impact(&self) -> &T::Signed {
433        &self.result.price_impact_value
434    }
435
436    /// Get the price impact amount.
437    pub fn price_impact_amount(&self) -> &T {
438        &self.result.price_impact_amount
439    }
440}
441
442struct ReassignedValues<T: Unsigned> {
443    long_token_delta_value: T::Signed,
444    short_token_delta_value: T::Signed,
445    token_in_price: Price<T>,
446    token_out_price: Price<T>,
447    long_pnl_factor_kind: PnlFactorKind,
448    short_pnl_factor_kind: PnlFactorKind,
449}
450
451impl<T: Unsigned> ReassignedValues<T> {
452    fn new(
453        long_token_delta_value: T::Signed,
454        short_token_delta_value: T::Signed,
455        token_in_price: Price<T>,
456        token_out_price: Price<T>,
457        long_pnl_factor_kind: PnlFactorKind,
458        short_pnl_factor_kind: PnlFactorKind,
459    ) -> Self {
460        Self {
461            long_token_delta_value,
462            short_token_delta_value,
463            token_in_price,
464            token_out_price,
465            long_pnl_factor_kind,
466            short_pnl_factor_kind,
467        }
468    }
469}
470
471struct Cache<'a, M, const DECIMALS: u8>
472where
473    M: BaseMarket<DECIMALS>,
474{
475    market: &'a M,
476    liquidity: M::Pool,
477    swap_impact: M::Pool,
478    claimable_fee: M::Pool,
479}
480
481impl<M, const DECIMALS: u8> BaseMarket<DECIMALS> for Cache<'_, M, DECIMALS>
482where
483    M: BaseMarket<DECIMALS>,
484{
485    type Num = M::Num;
486
487    type Signed = M::Signed;
488
489    type Pool = M::Pool;
490
491    fn liquidity_pool(&self) -> crate::Result<&Self::Pool> {
492        Ok(&self.liquidity)
493    }
494
495    fn claimable_fee_pool(&self) -> crate::Result<&Self::Pool> {
496        Ok(&self.claimable_fee)
497    }
498
499    fn swap_impact_pool(&self) -> crate::Result<&Self::Pool> {
500        Ok(&self.swap_impact)
501    }
502
503    fn open_interest_pool(&self, is_long: bool) -> crate::Result<&Self::Pool> {
504        self.market.open_interest_pool(is_long)
505    }
506
507    fn open_interest_in_tokens_pool(&self, is_long: bool) -> crate::Result<&Self::Pool> {
508        self.market.open_interest_in_tokens_pool(is_long)
509    }
510
511    fn collateral_sum_pool(&self, is_long: bool) -> crate::Result<&Self::Pool> {
512        self.market.collateral_sum_pool(is_long)
513    }
514
515    fn usd_to_amount_divisor(&self) -> Self::Num {
516        self.market.usd_to_amount_divisor()
517    }
518
519    fn max_pool_amount(&self, is_long_token: bool) -> crate::Result<Self::Num> {
520        self.market.max_pool_amount(is_long_token)
521    }
522
523    fn pnl_factor_config(&self, kind: PnlFactorKind, is_long: bool) -> crate::Result<Self::Num> {
524        self.market.pnl_factor_config(kind, is_long)
525    }
526
527    fn reserve_factor(&self) -> crate::Result<Self::Num> {
528        self.market.reserve_factor()
529    }
530
531    fn open_interest_reserve_factor(&self) -> crate::Result<Self::Num> {
532        self.market.open_interest_reserve_factor()
533    }
534
535    fn max_open_interest(&self, is_long: bool) -> crate::Result<Self::Num> {
536        self.market.max_open_interest(is_long)
537    }
538
539    fn ignore_open_interest_for_usage_factor(&self) -> crate::Result<bool> {
540        self.market.ignore_open_interest_for_usage_factor()
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use crate::{
547        market::{LiquidityMarketMutExt, SwapMarketMutExt},
548        pool::Balance,
549        price::Prices,
550        test::TestMarket,
551        BaseMarket, LiquidityMarket, MarketAction,
552    };
553
554    #[test]
555    fn basic() -> crate::Result<()> {
556        let mut market = TestMarket::<u64, 9>::default();
557        let mut prices = Prices::new_for_test(120, 120, 1);
558        market.deposit(1_000_000_000, 0, prices)?.execute()?;
559        prices.index_token_price.set_price_for_test(121);
560        prices.long_token_price.set_price_for_test(121);
561        market.deposit(1_000_000_000, 0, prices)?.execute()?;
562        prices.index_token_price.set_price_for_test(122);
563        prices.long_token_price.set_price_for_test(122);
564        market.deposit(0, 1_000_000_000, prices)?.execute()?;
565        println!("{market:#?}");
566
567        let prices = Prices::new_for_test(123, 123, 1);
568
569        // Test for positive impact.
570        let before_market = market.clone();
571        let token_in_amount = 100_000_000;
572        let report = market.swap(false, token_in_amount, prices)?.execute()?;
573        println!("{report:#?}");
574        println!("{market:#?}");
575
576        assert_eq!(before_market.total_supply(), market.total_supply());
577
578        assert_eq!(
579            before_market.liquidity_pool()?.long_amount()?,
580            market.liquidity_pool()?.long_amount()? + report.token_out_amount()
581                - report.price_impact_amount(),
582        );
583        assert_eq!(
584            before_market.liquidity_pool()?.short_amount()? + token_in_amount
585                - report.token_in_fees().fee_amount_for_receiver(),
586            market.liquidity_pool()?.short_amount()?,
587        );
588
589        assert_eq!(
590            before_market.swap_impact_pool()?.long_amount()?,
591            market.swap_impact_pool()?.long_amount()? + report.price_impact_amount(),
592        );
593        assert_eq!(
594            before_market.swap_impact_pool()?.short_amount()?,
595            market.swap_impact_pool()?.short_amount()?
596        );
597
598        assert_eq!(
599            before_market.claimable_fee_pool()?.long_amount()?,
600            market.claimable_fee_pool()?.long_amount()?,
601        );
602        assert_eq!(
603            before_market.claimable_fee_pool()?.short_amount()?
604                + report.token_in_fees().fee_amount_for_receiver(),
605            market.claimable_fee_pool()?.short_amount()?,
606        );
607
608        // Test for negative impact.
609        let before_market = market.clone();
610        let token_in_amount = 100_000;
611
612        let prices = Prices::new_for_test(119, 119, 1);
613
614        let report = market.swap(true, token_in_amount, prices)?.execute()?;
615        println!("{report:#?}");
616        println!("{market:#?}");
617
618        assert_eq!(before_market.total_supply(), market.total_supply());
619
620        assert_eq!(
621            before_market.liquidity_pool()?.long_amount()? + token_in_amount
622                - report.price_impact_amount()
623                - report.token_in_fees().fee_amount_for_receiver(),
624            market.liquidity_pool()?.long_amount()?,
625        );
626        assert_eq!(
627            before_market.liquidity_pool()?.short_amount()? - report.token_out_amount(),
628            market.liquidity_pool()?.short_amount()?,
629        );
630
631        assert_eq!(
632            before_market.swap_impact_pool()?.long_amount()? + report.price_impact_amount(),
633            market.swap_impact_pool()?.long_amount()?,
634        );
635        assert_eq!(
636            before_market.swap_impact_pool()?.short_amount()?,
637            market.swap_impact_pool()?.short_amount()?
638        );
639
640        assert_eq!(
641            before_market.claimable_fee_pool()?.long_amount()?
642                + report.token_in_fees().fee_amount_for_receiver(),
643            market.claimable_fee_pool()?.long_amount()?,
644        );
645        assert_eq!(
646            before_market.claimable_fee_pool()?.short_amount()?,
647            market.claimable_fee_pool()?.short_amount()?,
648        );
649        Ok(())
650    }
651
652    /// A test for zero swap.
653    #[test]
654    fn zero_amount_swap() -> crate::Result<()> {
655        let mut market = TestMarket::<u64, 9>::default();
656        let prices = Prices::new_for_test(120, 120, 1);
657        market.deposit(1_000_000_000, 0, prices)?.execute()?;
658        market.deposit(0, 1_000_000_000, prices)?.execute()?;
659        println!("{market:#?}");
660
661        let result = market.swap(true, 0, prices);
662        assert!(result.is_err());
663        println!("{market:#?}");
664
665        Ok(())
666    }
667
668    /// A test for over amount.
669    #[test]
670    fn over_amount_swap() -> crate::Result<()> {
671        let mut market = TestMarket::<u64, 9>::default();
672        let prices = Prices::new_for_test(120, 120, 1);
673        market.deposit(1_000_000_000, 0, prices)?.execute()?;
674        market.deposit(0, 1_000_000_000, prices)?.execute()?;
675        println!("{market:#?}");
676
677        let result = market.swap(true, 2_000_000_000, prices)?.execute();
678        assert!(result.is_err());
679        println!("{market:#?}");
680
681        // Try to swap out all long token.
682        let token_in_amount =
683            market.liquidity_pool()?.long_amount()? * prices.long_token_price.mid();
684        let report = market.swap(false, token_in_amount, prices)?.execute()?;
685        println!("{report:#?}");
686        println!("{market:#?}");
687
688        Ok(())
689    }
690
691    /// A test for small amount.
692    #[test]
693    fn small_amount_swap() -> crate::Result<()> {
694        let mut market = TestMarket::<u64, 9>::default();
695        let prices = Prices::new_for_test(120, 120, 1);
696        market.deposit(1_000_000_000, 0, prices)?.execute()?;
697        println!("{market:#?}");
698
699        let small_amount = 1;
700
701        let report = market.swap(false, small_amount, prices)?.execute()?;
702        println!("{report:#?}");
703        println!("{market:#?}");
704        assert!(market.liquidity_pool()?.short_amount()? != 0);
705
706        let report = market
707            .swap(false, prices.long_token_price.mid() * small_amount, prices)?
708            .execute()?;
709        println!("{report:#?}");
710        println!("{market:#?}");
711
712        // Test for round.
713        let report = market.swap(false, 200, prices)?.execute()?;
714        println!("{report:#?}");
715        println!("{market:#?}");
716
717        Ok(())
718    }
719}