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