Skip to main content

gmsol_sdk/simulation/
order.rs

1use std::{collections::BTreeMap, sync::Arc};
2
3use gmsol_model::{
4    action::{
5        decrease_position::{DecreasePositionFlags, DecreasePositionReport},
6        increase_position::IncreasePositionReport,
7        swap::SwapReport,
8    },
9    num::MulDiv,
10    price::Price,
11    utils::apply_factor,
12    MarketAction, PositionMutExt,
13};
14use gmsol_programs::{
15    constants::{MARKET_DECIMALS, MARKET_USD_UNIT},
16    gmsol_store::accounts::Position,
17    model::{MarketModel, PositionModel, VirtualInventoryModel},
18};
19use rust_decimal::prelude::Zero;
20use solana_sdk::pubkey::Pubkey;
21use typed_builder::TypedBuilder;
22
23use crate::builders::order::{CreateOrderKind, CreateOrderParams};
24
25use super::simulator::{SimulationOptions, Simulator, SwapOutput};
26
27/// Order simulation output.
28#[derive(Debug)]
29pub enum OrderSimulationOutput {
30    /// Increase output.
31    Increase {
32        swaps: Vec<SwapReport<u128, i128>>,
33        report: Box<IncreasePositionReport<u128, i128>>,
34        position: PositionModel,
35    },
36    /// Decrease output.
37    Decrease {
38        swaps: Vec<SwapReport<u128, i128>>,
39        report: Box<DecreasePositionReport<u128, i128>>,
40        position: PositionModel,
41    },
42    /// Swap output.
43    Swap(SwapOutput),
44}
45
46/// Order execution simulation.
47#[derive(Debug, TypedBuilder)]
48pub struct OrderSimulation<'a> {
49    simulator: &'a mut Simulator,
50    kind: CreateOrderKind,
51    params: &'a CreateOrderParams,
52    collateral_or_swap_out_token: &'a Pubkey,
53    #[builder(default)]
54    pay_token: Option<&'a Pubkey>,
55    #[builder(default)]
56    receive_token: Option<&'a Pubkey>,
57    #[builder(default)]
58    swap_path: &'a [Pubkey],
59    #[builder(default)]
60    position: Option<&'a Arc<Position>>,
61}
62
63/// Options for prices update.
64#[derive(Debug, Default, Clone)]
65pub struct UpdatePriceOptions {
66    /// Whether to prefer swap in token update.
67    pub prefer_swap_in_token_update: bool,
68    /// Allowed slippage for limit swap price.
69    pub limit_swap_slippage: Option<u128>,
70}
71
72impl OrderSimulation<'_> {
73    /// Execute the simulation with the given options.
74    pub fn execute_with_options(
75        self,
76        options: SimulationOptions,
77    ) -> crate::Result<OrderSimulationOutput> {
78        match self.kind {
79            CreateOrderKind::MarketIncrease | CreateOrderKind::LimitIncrease => {
80                self.increase(options)
81            }
82            CreateOrderKind::MarketDecrease
83            | CreateOrderKind::LimitDecrease
84            | CreateOrderKind::StopLossDecrease => self.decrease(options),
85            CreateOrderKind::MarketSwap | CreateOrderKind::LimitSwap => self.swap(options),
86        }
87    }
88
89    fn get_market(&self) -> crate::Result<&MarketModel> {
90        let market_token = &self.params.market_token;
91        self.simulator.get_market(market_token).ok_or_else(|| {
92            crate::Error::custom(format!(
93                "[sim] market `{market_token}` not found in the simulator"
94            ))
95        })
96    }
97
98    /// Update the prices in the simulator to execute limit orders.
99    pub fn update_prices(self, options: UpdatePriceOptions) -> crate::Result<Self> {
100        const DEFAULT_LIMIT_SWAP_SLIPPAGE: u128 = MARKET_USD_UNIT * 5 / 1000;
101
102        match self.kind {
103            CreateOrderKind::LimitIncrease
104            | CreateOrderKind::LimitDecrease
105            | CreateOrderKind::StopLossDecrease => {
106                let Some(trigger_price) = self.params.trigger_price else {
107                    return Err(crate::Error::custom("[sim] trigger price is required"));
108                };
109                let token = self.get_market()?.meta.index_token_mint;
110                let price = Price {
111                    min: trigger_price,
112                    max: trigger_price,
113                };
114                // NOTE: Collateral token price update not supported yet; may be in future.
115                self.simulator.insert_price(&token, Arc::new(price))?;
116            }
117            CreateOrderKind::LimitSwap => {
118                let swap_in = *self.pay_token.unwrap_or(self.collateral_or_swap_out_token);
119                let swap_out = *self.collateral_or_swap_out_token;
120                let swap_in_amount = self.params.amount;
121                let swap_out_amount = self.params.min_output;
122                let swap_in_price = self.simulator.get_price(&swap_in).ok_or_else(|| {
123                    crate::Error::custom(format!("[sim] price for {swap_in} is not ready"))
124                })?;
125                let swap_out_price = self.simulator.get_price(&swap_out).ok_or_else(|| {
126                    crate::Error::custom(format!("[sim] price for {swap_out} is not ready"))
127                })?;
128                let slippage = options
129                    .limit_swap_slippage
130                    .unwrap_or(DEFAULT_LIMIT_SWAP_SLIPPAGE);
131                if options.prefer_swap_in_token_update {
132                    let mut swap_in_price = swap_out_amount
133                        .checked_mul_div_ceil(&swap_out_price.max, &swap_in_amount)
134                        .ok_or_else(|| {
135                            crate::Error::custom(
136                                "failed to calculate trigger price for swap in token",
137                            )
138                        })?;
139                    let factor = MARKET_USD_UNIT.checked_add(slippage).ok_or_else(|| {
140                        crate::Error::custom(
141                            "[sim] failed to calculate factor for applying slippage",
142                        )
143                    })?;
144                    swap_in_price = apply_factor::<_, { MARKET_DECIMALS }>(&swap_in_price, &factor)
145                        .ok_or_else(|| {
146                            crate::Error::custom("[sim] failed to apply slippage to swap in price")
147                        })?;
148                    self.simulator.insert_price(
149                        &swap_in,
150                        Arc::new(Price {
151                            min: swap_in_price,
152                            max: swap_in_price,
153                        }),
154                    )?;
155                } else {
156                    let factor = MARKET_USD_UNIT.checked_sub(slippage).ok_or_else(|| {
157                        crate::Error::custom(
158                            "[sim] failed to calculate factor for applying slippage",
159                        )
160                    })?;
161                    let mut swap_out_price = swap_in_amount
162                        .checked_mul_div_ceil(&swap_in_price.min, &swap_out_amount)
163                        .ok_or_else(|| {
164                            crate::Error::custom(
165                                "failed to calculate trigger price for swap out token",
166                            )
167                        })?;
168                    swap_out_price =
169                        apply_factor::<_, { MARKET_DECIMALS }>(&swap_out_price, &factor)
170                            .ok_or_else(|| {
171                                crate::Error::custom(
172                                    "[sim] failed to apply slippage to swap out price",
173                                )
174                            })?;
175                    self.simulator.insert_price(
176                        &swap_out,
177                        Arc::new(Price {
178                            min: swap_out_price,
179                            max: swap_out_price,
180                        }),
181                    )?;
182                }
183            }
184            _ => {}
185        }
186        Ok(self)
187    }
188
189    fn increase(self, options: SimulationOptions) -> crate::Result<OrderSimulationOutput> {
190        let Self {
191            kind,
192            simulator,
193            params,
194            collateral_or_swap_out_token,
195            position,
196            swap_path,
197            pay_token,
198            ..
199        } = self;
200
201        let prices = simulator.get_prices_for_market(&params.market_token)?;
202
203        if matches!(kind, CreateOrderKind::LimitIncrease) && !options.skip_limit_price_validation {
204            let Some(trigger_price) = params.trigger_price else {
205                return Err(crate::Error::custom("[sim] trigger price is required"));
206            };
207
208            // Validate with trigger price.
209            let index_price = &prices.index_token_price;
210            if params.is_long {
211                let price = index_price.pick_price(true);
212                if *price > trigger_price {
213                    return Err(crate::Error::custom(format!(
214                        "[sim] index price must be <= trigger price for a increase-long order, but {price} > {trigger_price}."
215                    )));
216                }
217            } else {
218                let price = index_price.pick_price(false);
219                if *price < trigger_price {
220                    return Err(crate::Error::custom(format!(
221                        "[sim] index price must be >= trigger price for a increase-short order, but {price} < {trigger_price}."
222                    )));
223                }
224            }
225        }
226
227        let source_token = pay_token.unwrap_or(collateral_or_swap_out_token);
228        let swap_output = simulator.swap_along_path_with_options(
229            swap_path,
230            source_token,
231            params.amount,
232            options.clone(),
233        )?;
234        if swap_output.output_token() != collateral_or_swap_out_token {
235            return Err(crate::Error::custom("[sim] invalid swap path"));
236        }
237
238        // Execute the increase against a cloned market model, while VI state
239        // is managed exclusively via the simulator's global VI map.
240        let market_snapshot = {
241            let market = simulator.get_market(&params.market_token).ok_or_else(|| {
242                crate::Error::custom(format!(
243                    "[sim] market `{}` not found in the simulator",
244                    params.market_token
245                ))
246            })?;
247            market.clone()
248        };
249
250        let swap_amount = swap_output.amount();
251        let vi_ctx = if options.disable_vis {
252            None
253        } else {
254            Some(simulator.vis_mut())
255        };
256
257        let (report, position) = with_vi_models_if_some(
258            &market_snapshot,
259            position,
260            vi_ctx,
261            params.is_long,
262            collateral_or_swap_out_token,
263            move |position_model: &mut PositionModel| {
264                let report = position_model
265                    .increase(prices, swap_amount, params.size, params.acceptable_price)?
266                    .execute()?;
267                Ok(report)
268            },
269        )?;
270
271        // Persist the evolved market model back into the simulator environment.
272        {
273            let storage = simulator
274                .get_market_mut(&params.market_token)
275                .expect("market storage must exist");
276            *storage = position.market_model().clone();
277        }
278
279        Ok(OrderSimulationOutput::Increase {
280            swaps: swap_output.reports,
281            report: Box::new(report),
282            position,
283        })
284    }
285
286    fn decrease(self, options: SimulationOptions) -> crate::Result<OrderSimulationOutput> {
287        let Self {
288            kind,
289            simulator,
290            params,
291            collateral_or_swap_out_token,
292            position,
293            swap_path,
294            receive_token,
295            ..
296        } = self;
297
298        let prices = simulator.get_prices_for_market(&params.market_token)?;
299
300        // Validate with trigger price.
301        if !options.skip_limit_price_validation {
302            let index_price = &prices.index_token_price;
303            let is_long = params.is_long;
304            match kind {
305                CreateOrderKind::LimitDecrease => {
306                    let Some(trigger_price) = params.trigger_price else {
307                        return Err(crate::Error::custom("[sim] trigger price is required"));
308                    };
309                    if is_long {
310                        let price = index_price.pick_price(false);
311                        if *price < trigger_price {
312                            return Err(crate::Error::custom(format!(
313                            "[sim] index price must be >= trigger price for a limit-decrease-long order, but {price} < {trigger_price}."
314                        )));
315                        }
316                    } else {
317                        let price = index_price.pick_price(true);
318                        if *price > trigger_price {
319                            return Err(crate::Error::custom(format!(
320                            "[sim] index price must be <= trigger price for a limit-decrease-short order, but {price} > {trigger_price}."
321                        )));
322                        }
323                    }
324                }
325                CreateOrderKind::StopLossDecrease => {
326                    let Some(trigger_price) = params.trigger_price else {
327                        return Err(crate::Error::custom("[sim] trigger price is required"));
328                    };
329                    if is_long {
330                        let price = index_price.pick_price(false);
331                        if *price > trigger_price {
332                            return Err(crate::Error::custom(format!(
333                            "[sim] index price must be <= trigger price for a stop-loss-decrease-long order, but {price} > {trigger_price}."
334                        )));
335                        }
336                    } else {
337                        let price = index_price.pick_price(true);
338                        if *price < trigger_price {
339                            return Err(crate::Error::custom(format!(
340                            "[sim] index price must be >= trigger price for a stop-loss-decrease-short order, but {price} < {trigger_price}."
341                        )));
342                        }
343                    }
344                }
345                _ => {}
346            }
347        }
348
349        let Some(position) = position else {
350            return Err(crate::Error::custom(
351                "[sim] position must be provided for decrease order",
352            ));
353        };
354        if position.collateral_token != *collateral_or_swap_out_token {
355            return Err(crate::Error::custom("[sim] collateral token mismatched"));
356        }
357
358        // Execute the decrease against a cloned market model, while VI state
359        // is managed exclusively via the simulator's global VI map.
360        let market_snapshot = {
361            let market = simulator.get_market(&params.market_token).ok_or_else(|| {
362                crate::Error::custom(format!(
363                    "[sim] market `{}` not found in the simulator",
364                    params.market_token
365                ))
366            })?;
367            market.clone()
368        };
369
370        let vi_ctx = if options.disable_vis {
371            None
372        } else {
373            Some(simulator.vis_mut())
374        };
375
376        let (report, mut position) = with_vi_models_if_some(
377            &market_snapshot,
378            Some(position),
379            vi_ctx,
380            params.is_long,
381            collateral_or_swap_out_token,
382            move |position_model: &mut PositionModel| {
383                let report = position_model
384                    .decrease(
385                        prices,
386                        params.size,
387                        params.acceptable_price,
388                        params.amount,
389                        DecreasePositionFlags {
390                            is_insolvent_close_allowed: false,
391                            is_liquidation_order: false,
392                            is_cap_size_delta_usd_allowed: false,
393                        },
394                    )?
395                    .set_swap(
396                        params
397                            .decrease_position_swap_type
398                            .map(Into::into)
399                            .unwrap_or_default(),
400                    )
401                    .execute()?;
402                Ok(report)
403            },
404        )?;
405
406        // Persist the evolved market model back into the simulator environment.
407        {
408            let storage = simulator
409                .get_market_mut(&params.market_token)
410                .expect("market storage must exist");
411            *storage = position.market_model().clone();
412        }
413
414        let swaps = if !report.output_amount().is_zero() {
415            let source_token = collateral_or_swap_out_token;
416            let swap_output = simulator.swap_along_path_with_options(
417                swap_path,
418                source_token,
419                *report.output_amount(),
420                options.clone(),
421            )?;
422            let receive_token = receive_token.unwrap_or(collateral_or_swap_out_token);
423            if swap_output.output_token() != receive_token {
424                return Err(crate::Error::custom(format!(
425                    "[sim] invalid swap path: output_token={}, receive_token={receive_token}",
426                    swap_output.output_token()
427                )));
428            }
429            // Ensure the market model of the position is in-sync with the simulator's.
430            position.set_market_model(
431                simulator
432                    .get_market(&params.market_token)
433                    .expect("market storage must exist"),
434            );
435            swap_output.reports
436        } else {
437            vec![]
438        };
439
440        Ok(OrderSimulationOutput::Decrease {
441            swaps,
442            report,
443            position,
444        })
445    }
446
447    fn swap(self, options: SimulationOptions) -> crate::Result<OrderSimulationOutput> {
448        let Self {
449            kind,
450            simulator,
451            params,
452            collateral_or_swap_out_token,
453            swap_path,
454            pay_token,
455            ..
456        } = self;
457
458        let swap_in = *pay_token.unwrap_or(collateral_or_swap_out_token);
459
460        let swap_output = simulator.swap_along_path_with_options(
461            swap_path,
462            &swap_in,
463            params.amount,
464            options.clone(),
465        )?;
466        if swap_output.output_token() != collateral_or_swap_out_token {
467            return Err(crate::Error::custom("[sim] invalid swap path"));
468        }
469
470        if matches!(kind, CreateOrderKind::LimitSwap) && !options.skip_limit_price_validation {
471            let output_amount = swap_output.amount();
472            let min_output_amount = params.min_output;
473            if output_amount < min_output_amount {
474                return Err(crate::Error::custom(format!("[sim] the limit swap output is too low, {output_amount} < min_output = {min_output_amount}. Has the limit price been reached?")));
475            }
476        }
477
478        Ok(OrderSimulationOutput::Swap(swap_output))
479    }
480}
481
482fn with_vi_models_if_some<T>(
483    market: &MarketModel,
484    position: Option<&Arc<Position>>,
485    vi_map: Option<&mut BTreeMap<Pubkey, VirtualInventoryModel>>,
486    is_long: bool,
487    collateral_token: &Pubkey,
488    f: impl FnOnce(&mut PositionModel) -> crate::Result<T>,
489) -> crate::Result<(T, PositionModel)> {
490    let mut market: MarketModel = market.clone();
491    let (output, mut position) = market.with_vis_if(vi_map, |market_in_scope| {
492        let mut position =
493            make_position_model(market_in_scope, position, is_long, collateral_token)?;
494        let output = f(&mut position)?;
495        *market_in_scope = position.market_model().clone();
496        crate::Result::Ok((output, position))
497    })?;
498    position.set_market_model(&market);
499    Ok((output, position))
500}
501
502fn make_position_model(
503    market: &MarketModel,
504    position: Option<&Arc<Position>>,
505    is_long: bool,
506    collateral_token: &Pubkey,
507) -> crate::Result<PositionModel> {
508    match position {
509        Some(position) => {
510            if position.collateral_token != *collateral_token {
511                return Err(crate::Error::custom("[sim] collateral token mismatched"));
512            }
513            Ok(PositionModel::new(market.clone(), position.clone())?)
514        }
515        None => Ok(market
516            .clone()
517            .into_empty_position(is_long, *collateral_token)?),
518    }
519}