gmsol_sdk/market_graph/simulation/
order.rs1use 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#[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 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 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 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}