Skip to main content

riptide_amm_math/oracle/
mod.rs

1#![allow(dead_code)]
2use core::cmp::min;
3
4use ethnum::I256;
5
6#[cfg(feature = "wasm")]
7use riptide_amm_macros::wasm_expose;
8
9use borsh::{BorshDeserialize, BorshSerialize};
10
11use super::{
12    error::{CoreError, AMOUNT_EXCEEDS_MAX_U32, ARITHMETIC_OVERFLOW},
13    quote::{Price, Quote, QuoteType},
14    token::{a_to_b, b_to_a},
15    U128,
16};
17
18mod amm;
19mod book;
20mod flat;
21mod skew;
22mod spread;
23
24pub use amm::LiquidityType;
25pub use book::BookSpacingType;
26pub use skew::{SkewExponent, SkewMode};
27
28use amm::{amm_liquidity, amm_price};
29use book::{book_liquidity, new_book_liquidity, BOOK_LIQUIDITY_LEVELS};
30use flat::flat_liquidity;
31use skew::{apply_skew_to_liquidity, compute_deviation_per_m};
32use spread::spread_liquidity;
33
34pub(crate) const LIQUIDITY_LEVELS: usize = 32;
35
36pub const ORACLE_DATA_LEN: usize = 276;
37pub const SKEW_LEN: usize = 32;
38pub const ORACLE_PAYLOAD_LEN: usize = 512;
39pub const SKEW_OFFSET: usize = ORACLE_PAYLOAD_LEN - SKEW_LEN;
40
41// Represents the liquidity on one side of the order book (price and amount of out_token).
42// This is explicitly a fixed-size array to avoid heap allocation (which Vec does).
43// The `len` field is the actual number of initialized elements in the array.
44#[allow(clippy::len_without_is_empty)]
45#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
46#[cfg_attr(feature = "wasm", wasm_expose)]
47pub struct SingleSideLiquidity {
48    items: [(u128, u64); LIQUIDITY_LEVELS],
49    len: usize,
50}
51
52impl SingleSideLiquidity {
53    pub fn new() -> Self {
54        Self {
55            items: [(0, 0); LIQUIDITY_LEVELS],
56            len: 0,
57        }
58    }
59
60    pub fn from_slice(slice: &[(u128, u64)]) -> Self {
61        let mut items = [(0, 0); LIQUIDITY_LEVELS];
62        items[..slice.len()].copy_from_slice(slice);
63        Self {
64            items,
65            len: slice.len(),
66        }
67    }
68
69    pub fn push(&mut self, item: (u128, u64)) -> bool {
70        if self.len < 32 {
71            self.items[self.len] = item;
72            self.len += 1;
73            true
74        } else {
75            false
76        }
77    }
78
79    pub fn len(&self) -> usize {
80        self.len
81    }
82
83    pub fn as_slice(&self) -> &[(u128, u64)] {
84        &self.items[..self.len]
85    }
86}
87
88#[cfg_attr(feature = "wasm", wasm_expose)]
89pub const PER_M_DENOMINATOR: i32 = 1_000_000;
90
91#[derive(Debug, Clone, Copy, Eq, PartialEq)]
92#[cfg_attr(true, derive(BorshDeserialize, BorshSerialize))]
93#[cfg_attr(feature = "wasm", wasm_expose)]
94pub enum OracleData {
95    Empty,
96    FlatPrice {
97        price_q64_64: u128,
98    },
99    SimpleSpread {
100        price_q64_64: u128,
101        bid_spread_per_m: i32,
102        ask_spread_per_m: i32,
103    },
104    OrderBook {
105        price_q64_64: u128,
106        // The spacing between each level of liquidity
107        spacing: BookSpacingType,
108        // Expressed as a fraction of the total reserves
109        bid_liquidity_per_m: [u32; BOOK_LIQUIDITY_LEVELS],
110        ask_liquidity_per_m: [u32; BOOK_LIQUIDITY_LEVELS],
111    },
112    AutomatedMarketMaker {
113        liquidity_type: LiquidityType,
114        bid_spread_per_m: i32,
115        ask_spread_per_m: i32,
116    },
117}
118
119#[derive(Debug, Clone, Copy, Eq, PartialEq)]
120pub struct OraclePayload {
121    pub data: OracleData,
122    pub skew: SkewMode,
123}
124
125impl BorshDeserialize for OraclePayload {
126    fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
127        const _: () = assert!(ORACLE_DATA_LEN + SKEW_LEN <= ORACLE_PAYLOAD_LEN);
128
129        let mut buf = [0u8; ORACLE_PAYLOAD_LEN];
130        reader.read_exact(&mut buf)?;
131        let mut data_slice = &buf[..ORACLE_DATA_LEN];
132        let data = OracleData::deserialize(&mut data_slice)?;
133        let mut skew_slice = &buf[SKEW_OFFSET..SKEW_OFFSET + SKEW_LEN];
134        let skew = SkewMode::deserialize(&mut skew_slice)?;
135        Ok(Self { data, skew })
136    }
137}
138
139#[inline(never)]
140fn build_base_liquidity(
141    data: &OracleData,
142    quote_type: QuoteType,
143    reserves_a: u64,
144    reserves_b: u64,
145) -> Result<SingleSideLiquidity, CoreError> {
146    match data {
147        OracleData::Empty => Ok(SingleSideLiquidity::new()),
148        OracleData::FlatPrice { price_q64_64 } => {
149            flat_liquidity(*price_q64_64, quote_type, reserves_a, reserves_b)
150        }
151        OracleData::SimpleSpread {
152            price_q64_64,
153            bid_spread_per_m,
154            ask_spread_per_m,
155        } => spread_liquidity(
156            *price_q64_64,
157            *bid_spread_per_m,
158            *ask_spread_per_m,
159            quote_type,
160            reserves_a,
161            reserves_b,
162        ),
163        OracleData::OrderBook {
164            price_q64_64,
165            spacing,
166            bid_liquidity_per_m,
167            ask_liquidity_per_m,
168        } => book_liquidity(
169            quote_type,
170            *price_q64_64,
171            *spacing,
172            bid_liquidity_per_m,
173            ask_liquidity_per_m,
174            reserves_a,
175            reserves_b,
176        ),
177        OracleData::AutomatedMarketMaker {
178            liquidity_type,
179            bid_spread_per_m,
180            ask_spread_per_m,
181        } => amm_liquidity(
182            *liquidity_type,
183            *bid_spread_per_m,
184            *ask_spread_per_m,
185            quote_type,
186            reserves_a,
187            reserves_b,
188        ),
189    }
190}
191
192pub(crate) fn build_liquidity(
193    payload: &OraclePayload,
194    quote_type: QuoteType,
195    reserves_a: u64,
196    reserves_b: u64,
197) -> Result<SingleSideLiquidity, CoreError> {
198    let liquidity = build_base_liquidity(&payload.data, quote_type, reserves_a, reserves_b)?;
199
200    let price = match &payload.data {
201        OracleData::Empty | OracleData::AutomatedMarketMaker { .. } => return Ok(liquidity),
202        OracleData::FlatPrice { price_q64_64 }
203        | OracleData::SimpleSpread { price_q64_64, .. }
204        | OracleData::OrderBook { price_q64_64, .. } => *price_q64_64,
205    };
206
207    let deviation = compute_deviation_per_m(price, reserves_a, reserves_b)?;
208    let skew_per_m = payload.skew.compute_skew_per_m(deviation, quote_type)?;
209    apply_skew_to_liquidity(liquidity, skew_per_m, quote_type)
210}
211
212pub(crate) fn build_price(
213    liquidity: &SingleSideLiquidity,
214    oracle: &OracleData,
215    quote_type: QuoteType,
216    reserves_a: u64,
217    reserves_b: u64,
218) -> Result<Price, CoreError> {
219    let best_price = liquidity
220        .as_slice()
221        .iter()
222        .find(|(_, liquidity)| *liquidity > 0)
223        .map(|(price, _)| *price)
224        .unwrap_or(0);
225
226    let oracle_price = match oracle {
227        OracleData::Empty => 0,
228        OracleData::FlatPrice { price_q64_64 } => *price_q64_64,
229        OracleData::SimpleSpread { price_q64_64, .. } => *price_q64_64,
230        OracleData::OrderBook { price_q64_64, .. } => *price_q64_64,
231        OracleData::AutomatedMarketMaker { liquidity_type, .. } => {
232            amm_price(*liquidity_type, reserves_a, reserves_b)?
233        }
234    };
235
236    let diff = if quote_type.a_to_b() {
237        I256::from(oracle_price)
238            .checked_sub(I256::from(best_price))
239            .ok_or(ARITHMETIC_OVERFLOW)?
240    } else {
241        I256::from(best_price)
242            .checked_sub(I256::from(oracle_price))
243            .ok_or(ARITHMETIC_OVERFLOW)?
244    };
245
246    let spread = if best_price > 0 {
247        diff.checked_mul(I256::from(PER_M_DENOMINATOR))
248            .ok_or(ARITHMETIC_OVERFLOW)?
249            .checked_div(I256::from(oracle_price))
250            .ok_or(ARITHMETIC_OVERFLOW)?
251            .try_into()
252            .map_err(|_| AMOUNT_EXCEEDS_MAX_U32)?
253    } else {
254        0
255    };
256
257    Ok(Price {
258        oracle_price_q64_64: oracle_price,
259        best_price_q64_64: best_price,
260        spread_per_m: spread,
261    })
262}
263
264pub(crate) fn consume_liquidity(
265    amount: u64,
266    quote_type: QuoteType,
267    liquidity: &SingleSideLiquidity,
268) -> Result<Quote, CoreError> {
269    let mut remaining_amount = amount;
270    let mut other_amount: u64 = 0;
271
272    // Start consuming the liquidity from the best price (first)
273    // Stop when either the liquidity runs out or the full amount is consumed
274    // `liquidity` represents the amount of output token that is available at the given price
275
276    for (price, liquidity) in liquidity.as_slice() {
277        if *price == 0 || *liquidity == 0 {
278            continue;
279        }
280
281        let (step_specified_amount, step_other_amount) = if quote_type.exact_in() {
282            // ExactIn
283            let max_amount = if quote_type.input_is_token_a() {
284                b_to_a(*liquidity, U128::from(*price), true)?
285            } else {
286                a_to_b(*liquidity, U128::from(*price), true)?
287            };
288
289            let step_specified_amount = min(remaining_amount, max_amount);
290            let step_other_amount = if quote_type.input_is_token_a() {
291                a_to_b(step_specified_amount, U128::from(*price), false)?
292            } else {
293                b_to_a(step_specified_amount, U128::from(*price), false)?
294            };
295
296            (step_specified_amount, min(step_other_amount, *liquidity))
297        } else {
298            // ExactOut
299
300            let step_specified_amount = min(remaining_amount, *liquidity);
301            let step_other_amount = if quote_type.output_is_token_a() {
302                a_to_b(step_specified_amount, U128::from(*price), true)?
303            } else {
304                b_to_a(step_specified_amount, U128::from(*price), true)?
305            };
306
307            (step_specified_amount, step_other_amount)
308        };
309
310        remaining_amount = remaining_amount
311            .checked_sub(step_specified_amount)
312            .ok_or(ARITHMETIC_OVERFLOW)?;
313        other_amount = other_amount
314            .checked_add(step_other_amount)
315            .ok_or(ARITHMETIC_OVERFLOW)?;
316
317        if remaining_amount == 0 {
318            break;
319        }
320    }
321
322    let consumed_amount = amount - remaining_amount;
323
324    let (amount_in, amount_out) = if quote_type.exact_in() {
325        (consumed_amount, other_amount)
326    } else {
327        (other_amount, consumed_amount)
328    };
329
330    Ok(Quote {
331        amount_in,
332        amount_out,
333        quote_type,
334    })
335}
336
337pub(crate) fn new_oracle_data(
338    oracle: &OracleData,
339    quote: &Quote,
340    reserves_a: u64,
341    reserves_b: u64,
342) -> Result<OracleData, CoreError> {
343    match oracle {
344        OracleData::Empty
345        | OracleData::FlatPrice { .. }
346        | OracleData::SimpleSpread { .. }
347        | OracleData::AutomatedMarketMaker { .. } => Ok(*oracle),
348        OracleData::OrderBook {
349            price_q64_64,
350            spacing,
351            bid_liquidity_per_m,
352            ask_liquidity_per_m,
353        } => new_book_liquidity(
354            quote,
355            *price_q64_64,
356            *spacing,
357            bid_liquidity_per_m,
358            ask_liquidity_per_m,
359            reserves_a,
360            reserves_b,
361        ),
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use rstest::rstest;
369
370    #[rstest]
371    fn test_empty(
372        #[values(true, false)] amount_is_token_a: bool,
373        #[values(true, false)] amount_is_input: bool,
374    ) {
375        let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
376        let payload = OraclePayload {
377            data: OracleData::Empty,
378            skew: SkewMode::None,
379        };
380        let liquidity = build_liquidity(&payload, quote_type, 1000, 1000).unwrap();
381        let quote = consume_liquidity(100, quote_type, &liquidity).unwrap();
382        assert_eq!(
383            quote,
384            Quote {
385                amount_in: 0,
386                amount_out: 0,
387                quote_type,
388            }
389        );
390    }
391
392    #[rstest]
393    #[case(OracleData::FlatPrice { price_q64_64: 100 }, QuoteType::TokenAExactIn, 100, 100, 0)]
394    #[case(OracleData::FlatPrice { price_q64_64: 100 }, QuoteType::TokenBExactIn, 100, 100, 0)]
395    #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: 10000, ask_spread_per_m: 20000 }, QuoteType::TokenAExactIn, 100, 99, 10000)]
396    #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: 10000, ask_spread_per_m: 20000 }, QuoteType::TokenBExactIn, 100, 102, 20000)]
397    #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: -10000, ask_spread_per_m: -20000 }, QuoteType::TokenAExactIn, 100, 101, -10000)]
398    #[case(OracleData::SimpleSpread { price_q64_64: 100, bid_spread_per_m: -10000, ask_spread_per_m: -20000 }, QuoteType::TokenBExactIn, 100, 98, -20000)]
399    fn test_build_price(
400        #[case] oracle: OracleData,
401        #[case] quote_type: QuoteType,
402        #[case] expected_oracle_price: u128,
403        #[case] expected_best_price: u128,
404        #[case] expected_spread_per_m: i32,
405    ) {
406        let reserves_a = 1_000_000;
407        let reserves_b = 1_000_000;
408
409        let payload = OraclePayload {
410            data: oracle,
411            skew: SkewMode::None,
412        };
413        let liquidity = build_liquidity(&payload, quote_type, reserves_a, reserves_b).unwrap();
414
415        let price = build_price(&liquidity, &oracle, quote_type, reserves_a, reserves_b).unwrap();
416
417        let expected = Price {
418            oracle_price_q64_64: expected_oracle_price,
419            best_price_q64_64: expected_best_price,
420            spread_per_m: expected_spread_per_m,
421        };
422
423        assert_eq!(price, expected);
424    }
425
426    #[rstest]
427    #[case(vec![500_000, 500_000, 0], 100, 100, 0)]
428    #[case(vec![0, 500_000, 500_000], 100, 99, 10000)]
429    #[case(vec![0, 0, 1_000_000], 100, 98, 20000)]
430    #[case(vec![0, 0, 0], 100, 0, 0)]
431    fn test_build_price_book(
432        #[case] liquidity: Vec<u32>,
433        #[case] expected_oracle_price: u128,
434        #[case] expected_best_price: u128,
435        #[case] expected_spread_per_m: i32,
436    ) {
437        let mut liquidity_per_m = [0u32; 32];
438        liquidity_per_m[..liquidity.len()].copy_from_slice(&liquidity);
439        let reserves_a = 1_000_000;
440        let reserves_b = 1_000_000;
441        let oracle = OracleData::OrderBook {
442            price_q64_64: 100,
443            spacing: BookSpacingType::Linear(10000),
444            bid_liquidity_per_m: liquidity_per_m,
445            ask_liquidity_per_m: [0; 32],
446        };
447
448        let payload = OraclePayload {
449            data: oracle,
450            skew: SkewMode::None,
451        };
452        let liquidity =
453            build_liquidity(&payload, QuoteType::TokenAExactIn, reserves_a, reserves_b).unwrap();
454
455        let price = build_price(
456            &liquidity,
457            &oracle,
458            QuoteType::TokenAExactIn,
459            reserves_a,
460            reserves_b,
461        )
462        .unwrap();
463
464        let expected = Price {
465            oracle_price_q64_64: expected_oracle_price,
466            best_price_q64_64: expected_best_price,
467            spread_per_m: expected_spread_per_m,
468        };
469
470        assert_eq!(price, expected);
471    }
472
473    #[rstest]
474    #[case(OracleData::Empty)]
475    #[case(OracleData::FlatPrice { price_q64_64: 1 << 64 })]
476    #[case(OracleData::SimpleSpread { price_q64_64: 1 << 64, bid_spread_per_m: 1000, ask_spread_per_m: 1000 })]
477    #[case(OracleData::AutomatedMarketMaker { liquidity_type: LiquidityType::ConstantProduct, bid_spread_per_m: 1000, ask_spread_per_m: 1000 })]
478    fn test_new_liquidity_unchanged(#[case] oracle: OracleData) {
479        let quote = Quote {
480            amount_in: 100,
481            amount_out: 100,
482            quote_type: QuoteType::TokenAExactIn,
483        };
484        let new_oracle = new_oracle_data(&oracle, &quote, 0, 0).unwrap();
485        assert_eq!(new_oracle, oracle);
486    }
487
488    // Selling exact token_a. The lower the the price, the less token_b you get.
489    #[rstest]
490    #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenAExactIn }))]
491    #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenAExactIn }))]
492    #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenAExactIn }))]
493    #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenAExactIn }))]
494    #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenAExactIn }))]
495    fn test_consume_liquidity_input_token_a(
496        #[case] amount: u64,
497        #[case] expected: Result<Quote, CoreError>,
498    ) {
499        let liquidity = SingleSideLiquidity::from_slice(&[
500            (1 << 64, 100),
501            ((1 << 64) / 2, 500),
502            ((1 << 64) / 4, 1000),
503        ]);
504        let quote = consume_liquidity(amount, QuoteType::TokenAExactIn, &liquidity);
505        assert_eq!(quote, expected);
506    }
507
508    // Selling exact token_b. The higher the the price, the less token_a you get.
509    #[rstest]
510    #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenBExactIn }))]
511    #[case(500, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenBExactIn }))]
512    #[case(1100, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenBExactIn }))]
513    #[case(2500, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenBExactIn }))]
514    #[case(10000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenBExactIn }))]
515    fn test_consume_liquidity_input_token_b(
516        #[case] amount: u64,
517        #[case] expected: Result<Quote, CoreError>,
518    ) {
519        let liquidity =
520            SingleSideLiquidity::from_slice(&[(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)]);
521        let quote = consume_liquidity(amount, QuoteType::TokenBExactIn, &liquidity);
522        assert_eq!(quote, expected);
523    }
524
525    // Buying exact token_a. The higher the the price, the more token_b you pay.
526    #[rstest]
527    #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenAExactOut }))]
528    #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenAExactOut }))]
529    #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenAExactOut }))]
530    #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenAExactOut }))]
531    #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenAExactOut }))]
532    fn test_consume_liquidity_output_token_a(
533        #[case] amount: u64,
534        #[case] expected: Result<Quote, CoreError>,
535    ) {
536        let liquidity =
537            SingleSideLiquidity::from_slice(&[(1 << 64, 100), (2 << 64, 500), (4 << 64, 1000)]);
538        let quote = consume_liquidity(amount, QuoteType::TokenAExactOut, &liquidity);
539        assert_eq!(quote, expected);
540    }
541
542    // Buying exact token_b. The lower the the price, the more token_a you pay.
543    #[rstest]
544    #[case(100, Ok(Quote { amount_in: 100, amount_out: 100, quote_type: QuoteType::TokenBExactOut }))]
545    #[case(300, Ok(Quote { amount_in: 500, amount_out: 300, quote_type: QuoteType::TokenBExactOut }))]
546    #[case(600, Ok(Quote { amount_in: 1100, amount_out: 600, quote_type: QuoteType::TokenBExactOut }))]
547    #[case(950, Ok(Quote { amount_in: 2500, amount_out: 950, quote_type: QuoteType::TokenBExactOut }))]
548    #[case(2000, Ok(Quote { amount_in: 5100, amount_out: 1600, quote_type: QuoteType::TokenBExactOut }))]
549    fn test_consume_liquidity_output_token_b(
550        #[case] amount: u64,
551        #[case] expected: Result<Quote, CoreError>,
552    ) {
553        let liquidity = SingleSideLiquidity::from_slice(&[
554            (1 << 64, 100),
555            ((1 << 64) / 2, 500),
556            ((1 << 64) / 4, 1000),
557        ]);
558        let quote = consume_liquidity(amount, QuoteType::TokenBExactOut, &liquidity);
559        assert_eq!(quote, expected);
560    }
561
562    #[rstest]
563    #[case((1 << 64) / 8, true, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::TokenAExactIn }))]
564    #[case((1 << 64) / 8, true, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::TokenAExactOut }))]
565    #[case(8 << 64, false, true, Ok(Quote { amount_in: 100, amount_out: 12, quote_type: QuoteType::TokenBExactIn }))]
566    #[case(8 << 64, false, false, Ok(Quote { amount_in: 13, amount_out: 100, quote_type: QuoteType::TokenBExactOut }))]
567    fn test_consume_liquidity_rounding_direction(
568        #[case] price: u128,
569        #[case] amount_is_token_a: bool,
570        #[case] amount_is_input: bool,
571        #[case] expected: Result<Quote, CoreError>,
572    ) {
573        let liquidity = SingleSideLiquidity::from_slice(&[(price, 1000)]);
574        let quote_type = QuoteType::new(amount_is_token_a, amount_is_input);
575        let result = consume_liquidity(100, quote_type, &liquidity);
576        assert_eq!(result, expected);
577    }
578
579    const PRICE_ONE: u128 = 1 << 64;
580
581    // build_liquidity applies skew to SingleSideLiquidity prices
582    // adjusted_price = price * (PER_M - skew) / PER_M
583    #[rstest]
584    #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 500, 500, SkewMode::None, false)] // balanced, no skew
585    #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 500, 500, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 500_000, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, false)] // balanced, skew=0
586    #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 750, 250, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 500_000, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, true)] // excess A, price down
587    #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 250, 750, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 500_000, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, false)] // excess B, price up
588    #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 750, 250, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 100_000, positive_ask_per_m: 100_000, negative_ask_per_m: 100_000 }, true)] // low intensity, still lowers
589    #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 750, 250, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 1_000_000, negative_bid_per_m: 1_000_000, positive_ask_per_m: 1_000_000, negative_ask_per_m: 1_000_000 }, true)] // max intensity, stronger shift
590    #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 750, 250, SkewMode::Polynomial { exponent: SkewExponent::Quadratic, positive_bid_per_m: 500_000, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, true)] // quadratic skew
591    #[case(OracleData::FlatPrice { price_q64_64: PRICE_ONE }, 750, 250, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 500_000, negative_bid_per_m: 500_000, positive_ask_per_m: 500_000, negative_ask_per_m: 500_000 }, true)] // FlatPrice also skewed
592    #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 750, 250, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, true)] // asymmetric, excess A
593    #[case(OracleData::SimpleSpread { price_q64_64: PRICE_ONE, bid_spread_per_m: 10_000, ask_spread_per_m: 10_000 }, 250, 750, SkewMode::Polynomial { exponent: SkewExponent::Linear, positive_bid_per_m: 100_000, negative_bid_per_m: 200_000, positive_ask_per_m: 300_000, negative_ask_per_m: 400_000 }, false)] // asymmetric, excess B
594    fn test_build_liquidity_skew(
595        #[case] oracle: OracleData,
596        #[case] reserves_a: u64,
597        #[case] reserves_b: u64,
598        #[case] skew_mode: SkewMode,
599        #[case] price_lower: bool,
600    ) {
601        let payload_skew = OraclePayload {
602            data: oracle,
603            skew: skew_mode,
604        };
605        let payload_none = OraclePayload {
606            data: oracle,
607            skew: SkewMode::None,
608        };
609        let result_skew = build_liquidity(
610            &payload_skew,
611            QuoteType::TokenAExactIn,
612            reserves_a,
613            reserves_b,
614        )
615        .unwrap();
616        let result_none = build_liquidity(
617            &payload_none,
618            QuoteType::TokenAExactIn,
619            reserves_a,
620            reserves_b,
621        )
622        .unwrap();
623        let (skew_price, _) = result_skew.as_slice()[0];
624        let (none_price, _) = result_none.as_slice()[0];
625        assert_eq!(skew_price < none_price, price_lower);
626    }
627
628    #[test]
629    fn test_build_liquidity_skew_empty() {
630        let payload = OraclePayload {
631            data: OracleData::Empty,
632            skew: SkewMode::Polynomial {
633                exponent: SkewExponent::Linear,
634                positive_bid_per_m: 500_000,
635                negative_bid_per_m: 500_000,
636                positive_ask_per_m: 500_000,
637                negative_ask_per_m: 500_000,
638            },
639        };
640        let result = build_liquidity(&payload, QuoteType::TokenAExactIn, 750, 250).unwrap();
641        assert_eq!(result.len(), 0);
642    }
643}