Skip to main content

gmsol_sdk/market_graph/simulation/
order.rs

1use std::{collections::HashMap, sync::Arc};
2
3use gmsol_model::{
4    action::decrease_position::DecreasePositionFlags,
5    num::MulDiv,
6    price::{Price, Prices},
7    MarketAction, PositionMutExt,
8};
9use gmsol_programs::{
10    gmsol_store::accounts::Position,
11    model::{MarketModel, PositionModel},
12};
13use rust_decimal::prelude::Zero;
14use solana_sdk::pubkey::Pubkey;
15use typed_builder::TypedBuilder;
16
17use crate::{
18    builders::order::{CreateOrderKind, CreateOrderParams},
19    market_graph::{simulation::SimulationOptions, MarketGraph},
20};
21
22pub use crate::simulation::order::OrderSimulationOutput;
23
24/// Order execution simulation.
25#[derive(Debug, Clone, TypedBuilder)]
26pub struct OrderSimulation<'a> {
27    graph: &'a MarketGraph,
28    kind: CreateOrderKind,
29    params: &'a CreateOrderParams,
30    collateral_or_swap_out_token: &'a Pubkey,
31    #[builder(default)]
32    pay_token: Option<&'a Pubkey>,
33    #[builder(default)]
34    receive_token: Option<&'a Pubkey>,
35    #[builder(default)]
36    swap_path: &'a [Pubkey],
37    #[builder(default)]
38    position: Option<&'a Arc<Position>>,
39}
40
41impl OrderSimulation<'_> {
42    /// Execute with options.
43    pub fn execute_with_options(
44        self,
45        options: SimulationOptions,
46    ) -> crate::Result<OrderSimulationOutput> {
47        match self.kind {
48            CreateOrderKind::MarketIncrease | CreateOrderKind::LimitIncrease => self.increase(),
49            CreateOrderKind::MarketDecrease
50            | CreateOrderKind::LimitDecrease
51            | CreateOrderKind::StopLossDecrease => self.decrease(),
52            CreateOrderKind::MarketSwap | CreateOrderKind::LimitSwap => self.swap(options),
53        }
54    }
55
56    fn market_model_with_prices(&self) -> crate::Result<(MarketModel, Prices<u128>)> {
57        let market_token = &self.params.market_token;
58        let model = self.graph.get_market(market_token).ok_or_else(|| {
59            crate::Error::custom(format!(
60                "[sim] market `{market_token}` not found in the graph"
61            ))
62        })?;
63        let prices = self.graph.get_prices(&model.meta).ok_or_else(|| {
64            crate::Error::custom(format!("[sim] prices for `{market_token}` are not ready"))
65        })?;
66
67        Ok((model.clone(), prices))
68    }
69
70    fn increase(self) -> crate::Result<OrderSimulationOutput> {
71        let (market, mut prices) = self.market_model_with_prices()?;
72
73        let Self {
74            kind,
75            graph,
76            params,
77            collateral_or_swap_out_token,
78            position,
79            swap_path,
80            pay_token,
81            ..
82        } = self;
83
84        if matches!(kind, CreateOrderKind::LimitIncrease) {
85            let Some(trigger_price) = params.trigger_price else {
86                return Err(crate::Error::custom("[sim] trigger price is required"));
87            };
88            let price = Price {
89                min: trigger_price,
90                max: trigger_price,
91            };
92            // NOTE: Collateral token price update not supported yet; may be in future.
93            prices.index_token_price = price;
94        }
95
96        let source_token = pay_token.unwrap_or(collateral_or_swap_out_token);
97        let swap_output = graph.swap_along_path(swap_path, source_token, params.amount)?;
98        if swap_output.output_token != *collateral_or_swap_out_token {
99            return Err(crate::Error::custom("[sim] invalid swap path"));
100        }
101
102        let mut market = market.clone();
103
104        let mut position = match position {
105            Some(position) => {
106                if position.collateral_token != *collateral_or_swap_out_token {
107                    return Err(crate::Error::custom("[sim] collateral token mismatched"));
108                }
109                market.with_vis_disabled(|market| {
110                    PositionModel::new(market.clone(), position.clone())
111                })?
112            }
113            None => market.with_vis_disabled(|market| {
114                market
115                    .clone()
116                    .into_empty_position(params.is_long, *collateral_or_swap_out_token)
117            })?,
118        };
119
120        let report = position
121            .increase(
122                prices,
123                swap_output.amount,
124                params.size,
125                params.acceptable_price,
126            )?
127            .execute()?;
128
129        Ok(OrderSimulationOutput::Increase {
130            swaps: swap_output.reports,
131            report: Box::new(report),
132            position,
133        })
134    }
135
136    fn decrease(self) -> crate::Result<OrderSimulationOutput> {
137        let (market, mut prices) = self.market_model_with_prices()?;
138
139        let Self {
140            kind,
141            graph,
142            params,
143            collateral_or_swap_out_token,
144            position,
145            swap_path,
146            receive_token,
147            ..
148        } = self;
149
150        if matches!(
151            kind,
152            CreateOrderKind::LimitDecrease | CreateOrderKind::StopLossDecrease
153        ) {
154            let Some(trigger_price) = params.trigger_price else {
155                return Err(crate::Error::custom("[sim] trigger price is required"));
156            };
157            let price = Price {
158                min: trigger_price,
159                max: trigger_price,
160            };
161            // NOTE: Collateral token price update not supported yet; may be in future.
162            prices.index_token_price = price;
163        }
164
165        let Some(position) = position else {
166            return Err(crate::Error::custom(
167                "[sim] position must be provided for decrease order",
168            ));
169        };
170        if position.collateral_token != *collateral_or_swap_out_token {
171            return Err(crate::Error::custom("[sim] collateral token mismatched"));
172        }
173        let mut market = market.clone();
174
175        let mut position = market
176            .with_vis_disabled(|market| PositionModel::new(market.clone(), position.clone()))?;
177
178        let report = position
179            .decrease(
180                prices,
181                params.size,
182                params.acceptable_price,
183                params.amount,
184                DecreasePositionFlags {
185                    is_insolvent_close_allowed: false,
186                    is_liquidation_order: false,
187                    is_cap_size_delta_usd_allowed: false,
188                },
189            )?
190            .set_swap(
191                params
192                    .decrease_position_swap_type
193                    .map(Into::into)
194                    .unwrap_or_default(),
195            )
196            .execute()?;
197
198        let swaps = if !report.output_amount().is_zero() {
199            let source_token = collateral_or_swap_out_token;
200            let swap_output =
201                graph.swap_along_path(swap_path, source_token, *report.output_amount())?;
202            let receive_token = receive_token.unwrap_or(collateral_or_swap_out_token);
203            if swap_output.output_token != *receive_token {
204                return Err(crate::Error::custom(format!(
205                    "[sim] invalid swap path: output_token={}, receive_token={receive_token}",
206                    swap_output.output_token
207                )));
208            }
209            swap_output.reports
210        } else {
211            vec![]
212        };
213
214        Ok(OrderSimulationOutput::Decrease {
215            swaps,
216            report,
217            position,
218        })
219    }
220
221    fn swap(self, options: SimulationOptions) -> crate::Result<OrderSimulationOutput> {
222        let Self {
223            kind,
224            graph,
225            params,
226            collateral_or_swap_out_token,
227            swap_path,
228            pay_token,
229            ..
230        } = self;
231
232        let swap_in = *pay_token.unwrap_or(collateral_or_swap_out_token);
233        let swap_out = *collateral_or_swap_out_token;
234        let swap_in_amount = params.amount;
235        let swap_out_amount = params.min_output;
236        let is_limit_swap = matches!(kind, CreateOrderKind::LimitSwap);
237
238        let mut price_map = HashMap::<_, _>::default();
239        if is_limit_swap {
240            let swap_in_price = graph.get_price(&swap_in).ok_or_else(|| {
241                crate::Error::custom(format!("[sim] price for {swap_in} is not ready"))
242            })?;
243            let swap_out_price = graph.get_price(&swap_out).ok_or_else(|| {
244                crate::Error::custom(format!("[sim] price for {swap_out} is not ready"))
245            })?;
246            if options.prefer_swap_in_token_update {
247                let swap_in_price = swap_out_amount
248                    .checked_mul_div_ceil(&swap_out_price.max, &swap_in_amount)
249                    .ok_or_else(|| {
250                        crate::Error::custom("failed to calculate trigger price for swap in token")
251                    })?;
252                price_map.insert(swap_in, swap_in_price);
253            } else {
254                let swap_out_price = swap_in_amount
255                    .checked_mul_div_ceil(&swap_in_price.min, &swap_out_amount)
256                    .ok_or_else(|| {
257                        crate::Error::custom("failed to calculate trigger price for swap in token")
258                    })?;
259                price_map.insert(swap_out, swap_out_price);
260            }
261        }
262
263        let swap_output = graph.swap_along_path_with_price_updater(
264            swap_path,
265            &swap_in,
266            params.amount,
267            |meta, prices| {
268                if !is_limit_swap {
269                    return Ok(());
270                }
271                if let Some(price) = price_map.get(&meta.long_token_mint) {
272                    prices.long_token_price.max = *price;
273                    prices.long_token_price.min = *price;
274                }
275                if let Some(price) = price_map.get(&meta.short_token_mint) {
276                    prices.short_token_price.max = *price;
277                    prices.short_token_price.min = *price;
278                }
279                Ok(())
280            },
281        )?;
282        if swap_output.output_token != *collateral_or_swap_out_token {
283            return Err(crate::Error::custom("[sim] invalid swap path"));
284        }
285
286        Ok(OrderSimulationOutput::Swap(swap_output))
287    }
288}