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#[derive(Debug)]
29pub enum OrderSimulationOutput {
30 Increase {
32 swaps: Vec<SwapReport<u128, i128>>,
33 report: Box<IncreasePositionReport<u128, i128>>,
34 position: PositionModel,
35 },
36 Decrease {
38 swaps: Vec<SwapReport<u128, i128>>,
39 report: Box<DecreasePositionReport<u128, i128>>,
40 position: PositionModel,
41 },
42 Swap(SwapOutput),
44}
45
46#[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#[derive(Debug, Default, Clone)]
65pub struct UpdatePriceOptions {
66 pub prefer_swap_in_token_update: bool,
68 pub limit_swap_slippage: Option<u128>,
70}
71
72impl OrderSimulation<'_> {
73 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 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 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(¶ms.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 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 let market_snapshot = {
241 let market = simulator.get_market(¶ms.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 {
273 let storage = simulator
274 .get_market_mut(¶ms.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(¶ms.market_token)?;
299
300 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 let market_snapshot = {
361 let market = simulator.get_market(¶ms.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 {
408 let storage = simulator
409 .get_market_mut(¶ms.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 position.set_market_model(
431 simulator
432 .get_market(¶ms.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}