1use std::{
2 collections::{hash_map::Entry, BTreeMap, HashMap, HashSet},
3 fmt,
4 sync::Arc,
5};
6
7use either::Either;
8use gmsol_model::price::{Price, Prices};
9use gmsol_programs::{
10 gmsol_store::types::MarketMeta,
11 model::{MarketModel, VirtualInventoryModel},
12};
13use gmsol_utils::pubkey::DEFAULT_PUBKEY;
14use petgraph::{
15 graph::{EdgeIndex, NodeIndex},
16 prelude::StableDiGraph,
17 visit::{EdgeRef, IntoNodeIdentifiers, NodeIndexable},
18};
19use rust_decimal::{Decimal, MathematicalOps};
20use solana_sdk::pubkey::Pubkey;
21
22#[allow(deprecated)]
23#[cfg(simulation)]
24use crate::market_graph::simulation::order::{OrderSimulation, OrderSimulationBuilder};
25
26#[cfg(simulation)]
27use crate::{
28 builders::order::{CreateOrderKind, CreateOrderParams},
29 serde::StringPubkey,
30 simulation::Simulator,
31};
32
33#[cfg(simulation)]
34pub use crate::simulation::SwapOutput;
35
36use self::estimation::SwapEstimation;
37
38pub use self::{
39 config::MarketGraphConfig, error::MarketGraphError, estimation::SwapEstimationParams,
40};
41
42pub mod estimation;
44
45pub mod config;
47
48pub mod error;
50
51#[deprecated(since = "0.8.0", note = "use `Simulator` instead")]
53#[cfg(simulation)]
54pub mod simulation;
55
56type Graph = StableDiGraph<Node, Edge>;
57
58#[deprecated(
60 since = "0.8.0",
61 note = "use `OrderSimulationBuilderForSimulator` instead"
62)]
63#[allow(deprecated)]
64#[cfg(simulation)]
65pub type OrderSimulationBuilderForGraph<'a> = OrderSimulationBuilder<
66 'a,
67 (
68 (&'a MarketGraph,),
69 (CreateOrderKind,),
70 (&'a CreateOrderParams,),
71 (&'a Pubkey,),
72 (),
73 (),
74 (),
75 (),
76 ),
77>;
78
79#[derive(Debug, Clone)]
80struct Node {
81 #[cfg_attr(not(simulation), allow(dead_code))]
82 token: Pubkey,
83 price: Option<Arc<Price<u128>>>,
84}
85
86impl Node {
87 fn new(token: Pubkey) -> Self {
88 Self { token, price: None }
89 }
90}
91
92#[derive(Debug, Clone)]
93struct Edge {
94 market_token: Pubkey,
95 estimated: Option<SwapEstimation>,
96}
97
98impl Edge {
99 fn new(market_token: Pubkey, estimated: Option<SwapEstimation>) -> Self {
100 Self {
101 market_token,
102 estimated,
103 }
104 }
105
106 fn cost(&self) -> Option<Decimal> {
107 Some(-self.estimated.as_ref()?.ln_exchange_rate)
108 }
109}
110
111#[derive(Debug, Clone)]
112struct IndexTokenState {
113 node: Node,
114 markets: HashSet<Pubkey>,
115}
116
117#[derive(Debug, Clone)]
118struct CollateralTokenState {
119 ix: NodeIndex,
120 markets: HashSet<Pubkey>,
121}
122
123#[derive(Debug, Clone)]
124struct MarketState {
125 market: MarketModel,
126 long_edge: EdgeIndex,
127 short_edge: EdgeIndex,
128}
129
130impl MarketState {
131 fn new(market: MarketModel, long_edge: EdgeIndex, short_edge: EdgeIndex) -> Self {
132 Self {
133 market,
134 long_edge,
135 short_edge,
136 }
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct MarketGraph {
143 index_tokens: HashMap<Pubkey, IndexTokenState>,
144 collateral_tokens: HashMap<Pubkey, CollateralTokenState>,
145 markets: HashMap<Pubkey, MarketState>,
146 graph: Graph,
147 config: MarketGraphConfig,
148 vis: BTreeMap<Pubkey, VirtualInventoryModel>,
149}
150
151type Distances = Vec<Option<Decimal>>;
152type Predecessors = Vec<Option<(NodeIndex, Pubkey)>>;
153
154impl Default for MarketGraph {
155 fn default() -> Self {
156 Self::with_config(MarketGraphConfig::default())
157 }
158}
159
160impl MarketGraph {
161 pub fn with_config(config: MarketGraphConfig) -> Self {
163 Self {
164 index_tokens: Default::default(),
165 collateral_tokens: Default::default(),
166 markets: Default::default(),
167 graph: Default::default(),
168 config,
169 vis: Default::default(),
170 }
171 }
172
173 pub fn insert_market(&mut self, market: MarketModel) -> bool {
177 self.insert_market_with_options(market, true)
178 }
179
180 pub fn insert_market_with_options(
184 &mut self,
185 market: MarketModel,
186 update_estimation: bool,
187 ) -> bool {
188 let key = market.meta.market_token_mint;
189 let (long_token_ix, short_token_ix) = self.insert_tokens_with_meta(&market.meta);
190 match self.markets.entry(key) {
191 Entry::Vacant(e) => {
192 let long_edge =
193 self.graph
194 .add_edge(long_token_ix, short_token_ix, Edge::new(key, None));
195 let short_edge =
196 self.graph
197 .add_edge(short_token_ix, long_token_ix, Edge::new(key, None));
198 e.insert(MarketState::new(market, long_edge, short_edge));
199 if update_estimation {
200 self.update_estimation(Some(&key));
201 }
202 true
203 }
204 Entry::Occupied(mut e) => {
205 let state = e.get_mut();
206 state.market = market;
207 if update_estimation {
208 self.update_estimation(Some(&key));
209 }
210 false
211 }
212 }
213 }
214
215 pub fn insert_vi_options(
217 &mut self,
218 vi_address: Pubkey,
219 vi: VirtualInventoryModel,
220 update_estimation: bool,
221 ) -> Option<VirtualInventoryModel> {
222 let old = self.vis.insert(vi_address, vi);
223 if update_estimation {
224 self.update_estimation(None);
225 }
226 old
227 }
228
229 pub fn get_vi(&self, vi_address: &Pubkey) -> Option<&VirtualInventoryModel> {
231 self.vis.get(vi_address)
232 }
233
234 pub fn remove_vi(&mut self, vi_address: &Pubkey) -> Option<VirtualInventoryModel> {
236 self.vis.remove(vi_address)
237 }
238
239 pub fn vis(&self) -> impl Iterator<Item = (&Pubkey, &VirtualInventoryModel)> {
241 self.vis.iter()
242 }
243
244 fn update_estimation(&mut self, only: Option<&Pubkey>) {
245 let markets = only
246 .map(|token| Either::Left(self.markets.get(token).into_iter()))
247 .unwrap_or_else(|| Either::Right(self.markets.values()));
248 for state in markets {
249 let prices = self.get_prices(&state.market.meta);
250 let vi_for_swaps = (state.market.virtual_inventory_for_swaps != DEFAULT_PUBKEY)
251 .then_some(state.market.virtual_inventory_for_swaps)
252 .and_then(|vi_addr| self.vis.get(&vi_addr))
253 .map(|vi| (state.market.virtual_inventory_for_swaps, vi));
254
255 let long_edge = self
256 .graph
257 .edge_weight_mut(state.long_edge)
258 .expect("internal: inconsistent market map");
259 long_edge.estimated = self
260 .config
261 .estimate(&state.market, true, prices, vi_for_swaps);
262 let short_edge = self
263 .graph
264 .edge_weight_mut(state.short_edge)
265 .expect("internal: inconsistent market map");
266 short_edge.estimated = self
267 .config
268 .estimate(&state.market, false, prices, vi_for_swaps);
269 }
270 }
271
272 pub(crate) fn update_token_price_state(&mut self, token: &Pubkey, price: Arc<Price<u128>>) {
273 if let Some(state) = self.index_tokens.get_mut(token) {
274 state.node.price = Some(price.clone());
275 }
276 if let Some(state) = self.collateral_tokens.get(token) {
277 self.graph
278 .node_weight_mut(state.ix)
279 .expect("internal: inconsistent token map")
280 .price = Some(price);
281 }
282 let related_markets_for_index_token = self
283 .index_tokens
284 .get(token)
285 .map(|state| state.markets.iter())
286 .into_iter()
287 .flatten();
288 let related_markets_for_collateral_token = self
289 .collateral_tokens
290 .get(token)
291 .map(|state| state.markets.iter())
292 .into_iter()
293 .flatten();
294 let related_markets = related_markets_for_index_token
295 .chain(related_markets_for_collateral_token)
296 .copied()
297 .collect::<HashSet<_>>();
298 for market_token in related_markets {
299 self.update_estimation(Some(&market_token));
300 }
301 }
302
303 pub fn update_token_price(&mut self, token: &Pubkey, price: &Price<u128>) {
307 self.update_token_price_state(token, Arc::new(*price));
308 }
309
310 pub fn update_value(&mut self, value: u128) {
312 let mut config = self.config;
313 config.swap_estimation_params.value = value;
314 self.update_config(config, true);
315 }
316
317 pub fn update_base_cost(&mut self, base_cost: u128) {
319 let mut config = self.config;
320 config.swap_estimation_params.base_cost = base_cost;
321 self.update_config(config, true);
322 }
323
324 pub fn update_max_steps(&mut self, max_steps: usize) {
326 self.update_config(
327 MarketGraphConfig {
328 max_steps,
329 ..self.config
330 },
331 false,
332 );
333 }
334
335 fn update_config(&mut self, config: MarketGraphConfig, should_update_estimation: bool) {
337 self.config = config;
338 if should_update_estimation {
339 self.update_estimation(None);
340 }
341 }
342
343 fn insert_collateral_token(&mut self, token: Pubkey, market_token: Pubkey) -> NodeIndex {
344 match self.collateral_tokens.entry(token) {
345 Entry::Vacant(e) => {
346 let ix = self.graph.add_node(Node::new(token));
347 let state = CollateralTokenState {
348 ix,
349 markets: HashSet::from([market_token]),
350 };
351 e.insert(state);
352 ix
353 }
354 Entry::Occupied(mut e) => {
355 e.get_mut().markets.insert(market_token);
356 e.get().ix
357 }
358 }
359 }
360
361 fn insert_index_token(&mut self, index_token: Pubkey, market_token: Pubkey) {
362 self.index_tokens
363 .entry(index_token)
364 .or_insert_with(|| IndexTokenState {
365 markets: HashSet::default(),
366 node: Node::new(index_token),
367 })
368 .markets
369 .insert(market_token);
370 }
371
372 fn insert_tokens_with_meta(&mut self, meta: &MarketMeta) -> (NodeIndex, NodeIndex) {
373 self.insert_index_token(meta.index_token_mint, meta.market_token_mint);
374 let long_token_ix =
375 self.insert_collateral_token(meta.long_token_mint, meta.market_token_mint);
376 let short_token_ix =
377 self.insert_collateral_token(meta.short_token_mint, meta.market_token_mint);
378 (long_token_ix, short_token_ix)
379 }
380
381 fn get_token_node(&self, token: &Pubkey) -> Option<&Node> {
382 if let Some(state) = self.index_tokens.get(token) {
383 Some(&state.node)
384 } else {
385 let state = self.collateral_tokens.get(token)?;
386 self.graph.node_weight(state.ix)
387 }
388 }
389
390 fn get_price(&self, token: &Pubkey) -> Option<Price<u128>> {
391 self.get_token_node(token)
392 .and_then(|node| node.price.as_deref().copied())
393 }
394
395 fn get_prices(&self, meta: &MarketMeta) -> Option<Prices<u128>> {
396 let index_token_price = self.get_price(&meta.index_token_mint)?;
397 let long_token_price = self.get_price(&meta.long_token_mint)?;
398 let short_token_price = self.get_price(&meta.short_token_mint)?;
399 Some(Prices {
400 index_token_price,
401 long_token_price,
402 short_token_price,
403 })
404 }
405
406 pub fn get_market(&self, market_token: &Pubkey) -> Option<&MarketModel> {
408 Some(&self.markets.get(market_token)?.market)
409 }
410
411 pub fn markets(&self) -> impl Iterator<Item = &MarketModel> {
413 self.markets.values().map(|state| &state.market)
414 }
415
416 pub fn market_tokens(&self) -> impl Iterator<Item = &Pubkey> {
418 self.markets.keys()
419 }
420
421 pub fn index_tokens(&self) -> impl Iterator<Item = &Pubkey> {
423 self.index_tokens.keys()
424 }
425
426 fn to_index(&self, ix: NodeIndex) -> usize {
427 self.graph.to_index(ix)
428 }
429
430 fn bellman_ford(&self, source: &Pubkey) -> crate::Result<(Distances, Predecessors)> {
435 let source = self
436 .collateral_tokens
437 .get(source)
438 .ok_or_else(|| crate::Error::custom("the source is not a known collateral token"))?
439 .ix;
440
441 let g = &self.graph;
442 let max_steps = self.config.max_steps;
443 let mut predecessors = vec![None; g.node_bound()];
444 let mut distances = vec![None; g.node_bound()];
445 distances[self.to_index(source)] = Some(Decimal::ZERO);
446
447 let mut result_distances = None;
448
449 for steps in 1..self.graph.node_count() {
450 let mut did_update = false;
451 for i in g.node_identifiers() {
452 for edge in g.edges(i) {
453 let j = edge.target();
454 let Some(w) = edge.weight().cost() else {
455 continue;
456 };
457 let Some(d) = distances[self.to_index(i)] else {
458 continue;
459 };
460 if distances[self.to_index(j)]
461 .map(|current| d + w < current)
462 .unwrap_or(true)
463 {
464 distances[self.to_index(j)] = distances[self.to_index(i)].map(|d| d + w);
465
466 if steps <= max_steps {
468 predecessors[self.to_index(j)] = Some((i, edge.weight().market_token));
469 }
470
471 did_update = true;
472 }
473 }
474 }
475
476 if !did_update {
477 break;
478 }
479
480 if steps == max_steps {
482 result_distances = Some(distances.clone());
483 }
484 }
485
486 for i in g.node_identifiers() {
488 for edge in g.edges(i) {
489 let j = edge.target();
490 let Some(w) = edge.weight().cost() else {
491 continue;
492 };
493 let Some(d) = distances[self.to_index(i)] else {
494 continue;
495 };
496 if distances[self.to_index(j)]
497 .map(|jd| d + w < jd)
498 .unwrap_or(true)
499 {
500 return Err(MarketGraphError::NegativeCycle.into());
501 }
502 }
503 }
504
505 Ok((result_distances.unwrap_or(distances), predecessors))
506 }
507
508 fn dfs(&self, source: &Pubkey) -> crate::Result<(Distances, Predecessors)> {
509 let source = self
510 .collateral_tokens
511 .get(source)
512 .ok_or_else(|| crate::Error::custom("the source is not a known collateral token"))?
513 .ix;
514
515 let g = &self.graph;
516 let mut predecessors = vec![None; g.node_bound()];
517 let mut distances = vec![None; g.node_bound()];
518 let mut visited_nodes = HashSet::<NodeIndex>::new();
519
520 self.dfs_recursive(
521 source,
522 Some(Decimal::ZERO),
523 None,
524 0,
525 &mut visited_nodes,
526 &mut distances,
527 &mut predecessors,
528 );
529
530 Ok((distances, predecessors))
531 }
532
533 #[allow(clippy::too_many_arguments)]
534 fn dfs_recursive(
535 &self,
536 current: NodeIndex,
537 distance: Option<Decimal>,
538 predecessor: Option<(NodeIndex, Pubkey)>,
539 steps: usize,
540 visited_nodes: &mut HashSet<NodeIndex>,
541 distances: &mut Distances,
542 predecessors: &mut Predecessors,
543 ) {
544 let i = current;
545 if steps > self.config.max_steps {
546 return;
547 }
548 let Some(d) = distance else {
549 return;
550 };
551 let best_d = distances[self.to_index(i)];
552 if best_d.map(|best| d >= best).unwrap_or(false) {
553 return;
554 }
555
556 visited_nodes.insert(i);
557
558 distances[self.to_index(i)] = Some(d);
559 predecessors[self.to_index(i)] = predecessor;
560
561 for edge in self.graph.edges(i) {
562 let j = edge.target();
563 if visited_nodes.contains(&j) {
564 continue;
565 }
566 self.dfs_recursive(
567 j,
568 edge.weight().cost().map(|w| w + d),
569 Some((i, edge.weight().market_token)),
570 steps + 1,
571 visited_nodes,
572 distances,
573 predecessors,
574 );
575 }
576
577 visited_nodes.remove(&i);
578 }
579
580 pub fn best_swap_paths(
582 &self,
583 source: &Pubkey,
584 skip_bellman_ford: bool,
585 ) -> crate::Result<BestSwapPaths<'_>> {
586 let (distances, predecessors, arbitrage_exists) = if skip_bellman_ford {
587 let results = self.dfs(source)?;
588 (results.0, results.1, None)
589 } else {
590 match self.bellman_ford(source) {
591 Ok(results) => (results.0, results.1, Some(false)),
592 Err(crate::Error::MarketGraph(MarketGraphError::NegativeCycle)) => {
594 let results = self.dfs(source)?;
595 (results.0, results.1, Some(true))
596 }
597 Err(err) => return Err(err),
598 }
599 };
600
601 Ok(BestSwapPaths {
602 graph: self,
603 source: *source,
604 distances,
605 predecessors,
606 arbitrage_exists,
607 })
608 }
609
610 #[deprecated(since = "0.8.0", note = "use `to_simulator` instead")]
612 #[cfg(simulation)]
613 pub fn swap_along_path_with_price_updater(
614 &self,
615 path: &[Pubkey],
616 source_token: &Pubkey,
617 mut amount: u128,
618 mut price_updater: impl FnMut(&MarketMeta, &mut Prices<u128>) -> crate::Result<()>,
619 ) -> crate::Result<SwapOutput> {
620 use crate::model::{MarketAction, SwapMarketMutExt};
621
622 let mut current_token = *source_token;
623
624 let mut reports = Vec::with_capacity(path.len());
625 for market_token in path {
626 let mut market = self
627 .get_market(market_token)
628 .ok_or_else(|| {
629 crate::Error::custom(format!(
630 "[swap] market `{market_token}` not found in the graph"
631 ))
632 })?
633 .clone();
634 let meta = &market.meta;
635 if meta.long_token_mint == meta.short_token_mint {
636 return Err(crate::Error::custom(format!(
637 "[swap] `{market_token} is not a swappable market"
638 )));
639 }
640 let is_token_in_long = if meta.long_token_mint == current_token {
641 current_token = meta.short_token_mint;
642 true
643 } else if meta.short_token_mint == current_token {
644 current_token = meta.long_token_mint;
645 false
646 } else {
647 return Err(crate::Error::custom(format!(
648 "[swap] invalid swap step. Current step: {market_token}"
649 )));
650 };
651 let mut prices = self.get_prices(meta).ok_or_else(|| {
652 crate::Error::custom(format!("[swap] prices for {market_token} are not ready"))
653 })?;
654 (price_updater)(meta, &mut prices)?;
655 let report = market.with_vis_disabled(|market| {
656 market.swap(is_token_in_long, amount, prices)?.execute()
657 })?;
658 amount = *report.token_out_amount();
659 reports.push(report);
660 }
661
662 Ok(SwapOutput {
663 output_token: current_token,
664 amount,
665 reports,
666 })
667 }
668
669 #[deprecated(since = "0.8.0", note = "use `to_simulator` instead")]
671 #[allow(deprecated)]
672 #[cfg(simulation)]
673 pub fn swap_along_path(
674 &self,
675 path: &[Pubkey],
676 source_token: &Pubkey,
677 amount: u128,
678 ) -> crate::Result<SwapOutput> {
679 self.swap_along_path_with_price_updater(path, source_token, amount, |_meta, _prices| Ok(()))
680 }
681
682 #[deprecated(since = "0.8.0", note = "use `to_simulator` instead")]
684 #[allow(deprecated)]
685 #[cfg(simulation)]
686 pub fn simulate_order<'a>(
687 &'a self,
688 kind: CreateOrderKind,
689 params: &'a CreateOrderParams,
690 collateral_or_swap_out_token: &'a Pubkey,
691 ) -> OrderSimulationBuilderForGraph<'a> {
692 OrderSimulation::builder()
693 .graph(self)
694 .kind(kind)
695 .params(params)
696 .collateral_or_swap_out_token(collateral_or_swap_out_token)
697 }
698
699 #[cfg(simulation)]
701 pub fn update_with_simulator(
702 &mut self,
703 simulator: &Simulator,
704 options: UpdateGraphWithSimulatorOptions,
705 ) {
706 let update_vis = !options.skip_vis_update;
707 let update_markets = !options.skip_markets_update;
708 let update_token_prices = options.update_token_prices;
709
710 if update_vis {
711 for (vi_address, vi) in simulator.vis() {
712 self.insert_vi_options(*vi_address, vi.clone(), false);
713 }
714 }
715
716 if update_markets {
717 for (_, market) in simulator.markets() {
718 self.insert_market_with_options(market.clone(), false);
719 }
720 }
721
722 if update_token_prices {
723 for (token, state) in simulator.tokens() {
724 if let Some(price) = state.price() {
725 self.update_token_price_state(token, price.clone());
726 }
727 }
728 } else {
729 self.update_estimation(None);
730 }
731 }
732
733 #[cfg(simulation)]
735 pub fn to_simulator(&self, options: CreateGraphSimulatorOptions) -> Simulator {
736 use crate::simulation::simulator::TokenState;
737
738 let market_filter = options.market_filter.as_ref();
739 let mut token_filter: Option<HashSet<_>> = None;
740 let markets = self
741 .markets
742 .iter()
743 .filter_map(|(market_token, state)| {
744 if let Some(filter) = market_filter {
745 if !filter.contains(market_token) {
746 return None;
747 }
748 let token_filter = token_filter.get_or_insert_default();
749 token_filter.insert(state.market.meta.index_token_mint);
750 token_filter.insert(state.market.meta.long_token_mint);
751 token_filter.insert(state.market.meta.short_token_mint);
752 };
753
754 Some((*market_token, state.market.clone()))
755 })
756 .collect();
757
758 let index_token_prices = self
759 .index_tokens
760 .iter()
761 .map(|(token, state)| (*token, state.node.price.clone()));
762 let collateral_token_prices = self
763 .graph
764 .node_weights()
765 .map(|node| (node.token, node.price.clone()));
766
767 let tokens = index_token_prices
768 .chain(collateral_token_prices)
769 .filter(|(token, _)| {
770 if let Some(token_filter) = token_filter.as_ref() {
771 token_filter.contains(token)
772 } else {
773 true
774 }
775 })
776 .map(|(token, price)| (token, TokenState::from_price(price)))
777 .collect();
778
779 crate::simulation::Simulator::from_parts(
780 tokens,
781 markets,
782 Default::default(),
783 self.vis.clone(),
784 )
785 }
786}
787
788#[cfg(simulation)]
790#[derive(Debug, Default, Clone)]
791#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
792#[cfg_attr(js, derive(tsify_next::Tsify))]
793#[cfg_attr(js, tsify(into_wasm_abi, from_wasm_abi))]
794pub struct CreateGraphSimulatorOptions {
795 #[cfg_attr(serde, serde(default))]
797 pub market_filter: Option<HashSet<StringPubkey>>,
798}
799
800#[cfg(simulation)]
802#[derive(Debug, Default, Clone)]
803#[cfg_attr(serde, derive(serde::Serialize, serde::Deserialize))]
804#[cfg_attr(js, derive(tsify_next::Tsify))]
805#[cfg_attr(js, tsify(into_wasm_abi, from_wasm_abi))]
806pub struct UpdateGraphWithSimulatorOptions {
807 #[cfg_attr(serde, serde(default))]
809 pub update_token_prices: bool,
810 #[cfg_attr(serde, serde(default))]
812 pub skip_markets_update: bool,
813 #[cfg_attr(serde, serde(default))]
815 pub skip_vis_update: bool,
816}
817
818pub struct BestSwapPaths<'a> {
820 graph: &'a MarketGraph,
821 source: Pubkey,
822 distances: Distances,
823 predecessors: Predecessors,
824 arbitrage_exists: Option<bool>,
825}
826
827impl fmt::Debug for BestSwapPaths<'_> {
828 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
829 f.debug_struct("BestSwapPaths")
830 .field("source", &self.source)
831 .field("distances", &self.distances)
832 .field("predecessors", &self.predecessors)
833 .field("arbitrage_exists", &self.arbitrage_exists)
834 .finish()
835 }
836}
837
838impl BestSwapPaths<'_> {
839 pub fn source(&self) -> &Pubkey {
841 &self.source
842 }
843
844 pub fn params(&self) -> &SwapEstimationParams {
846 &self.graph.config.swap_estimation_params
847 }
848
849 pub fn arbitrage_exists(&self) -> Option<bool> {
853 self.arbitrage_exists
854 }
855
856 pub fn to(&self, target: &Pubkey) -> (Option<Decimal>, Vec<Pubkey>) {
858 let Self {
859 graph,
860 distances,
861 predecessors,
862 source,
863 ..
864 } = self;
865
866 let Some(target_state) = graph.collateral_tokens.get(target) else {
867 return (None, vec![]);
868 };
869
870 let target_ix = target_state.ix;
871 let target_ix = graph.to_index(target_ix);
872
873 let distance = distances[target_ix];
874
875 if *source == *target {
876 return (distance.map(distance_to_exchange_rate), vec![]);
877 }
878
879 let mut path = vec![];
880 let mut current = predecessors[target_ix];
881 let mut steps = 0;
882 while let Some((predecessor, market_token)) = current.as_ref() {
883 steps += 1;
884 if steps > graph.config.max_steps {
885 return (None, vec![]);
886 }
887 path.push(*market_token);
888 current = predecessors[graph.to_index(*predecessor)];
889 }
890
891 path.reverse();
892
893 (
894 if path.is_empty() {
895 None
897 } else {
898 distance.map(distance_to_exchange_rate)
899 },
900 path,
901 )
902 }
903}
904
905fn distance_to_exchange_rate(d: Decimal) -> Decimal {
906 (-d).exp()
907}
908
909#[cfg(test)]
910mod tests {
911 use std::sync::Arc;
912
913 use gmsol_programs::gmsol_store::accounts::Market;
914 use petgraph::dot::Dot;
915
916 use crate::{
917 constants,
918 utils::{test::setup_fmt_tracing, zero_copy::try_deserialize_zero_copy_from_base64},
919 };
920
921 use super::*;
922
923 const USDC: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
924 const WSOL: &str = "So11111111111111111111111111111111111111112";
925 const BOME: &str = "ukHH6c7mMyiWCf1b9pnWe25TSpkDDt3H5pQZgZ74J82";
926
927 #[cfg(simulation)]
928 use crate::glv::model::GlvModel;
929 #[cfg(simulation)]
930 const SOL_BALANCED_MARKET_TOKEN: &str = "BwN2FWixP5JyKjJNyD1YcRKN1XhgvFtnzrPrkfyb4DkW";
931 #[cfg(simulation)]
932 const BNB_BALANCED_MARKET_TOKEN: &str = "J42dzcHkgmzU7MARv7k29TrkKaGQ1j2mTaYjkBkN7pn5";
933 #[cfg(simulation)]
934 const AAVE_BALANCED_MARKET_TOKEN: &str = "2dVHXNgzC7vvcsDi89S6crSkX3Y6HzPgZfmWqttEzvmo";
935
936 fn get_market_updates() -> Vec<(String, u64)> {
937 const DATA: &str = include_str!("test_data/markets.csv");
938 DATA.trim()
939 .split('\n')
940 .enumerate()
941 .map(|(idx, data)| {
942 let mut data = data.split(',');
943 let _market_token = data
944 .next()
945 .unwrap_or_else(|| panic!("[{idx}] missing market_token"));
946 let market = data
947 .next()
948 .unwrap_or_else(|| panic!("[{idx}] missing market data"));
949 let supply = data
950 .next()
951 .unwrap_or_else(|| panic!("[{idx}] missing supply"));
952
953 (
954 market.to_string(),
955 supply
956 .parse()
957 .unwrap_or_else(|_| panic!("[{idx}] invalid supply format")),
958 )
959 })
960 .collect()
961 }
962
963 fn get_price_updates() -> Vec<(i64, Pubkey, Price<u128>)> {
964 const DATA: &str = include_str!("test_data/prices.csv");
965 DATA.trim()
966 .split('\n')
967 .enumerate()
968 .map(|(idx, data)| {
969 let mut data = data.split(',');
970 let ts = data.next().unwrap_or_else(|| panic!("[{idx}] missing ts"));
971 let token = data
972 .next()
973 .unwrap_or_else(|| panic!("[{idx}] missing token"));
974 let min = data
975 .next()
976 .unwrap_or_else(|| panic!("[{idx}] missing min price"));
977 let max = data
978 .next()
979 .unwrap_or_else(|| panic!("[{idx}] missing max price"));
980 (
981 ts.parse()
982 .unwrap_or_else(|_| panic!("[{idx}] invalid ts format")),
983 token
984 .parse()
985 .unwrap_or_else(|_| panic!("[{idx}] invalid token format")),
986 Price {
987 min: min
988 .parse()
989 .unwrap_or_else(|_| panic!("[{idx}] invalid min price format")),
990 max: max
991 .parse()
992 .unwrap_or_else(|_| panic!("[{idx}] invalid max price format")),
993 },
994 )
995 })
996 .collect()
997 }
998
999 #[cfg(simulation)]
1000 fn get_glv() -> (Pubkey, GlvModel) {
1001 use gmsol_programs::{
1002 bytemuck::Zeroable,
1003 constants::MARKET_USD_UNIT,
1004 gmsol_store::{accounts::Glv, types::GlvMarketConfig},
1005 };
1006
1007 const MARKET_TOKEN_UNIT: u64 = 1_000_000_000;
1008
1009 let usdc: Pubkey = USDC.parse().unwrap();
1010 let wsol: Pubkey = WSOL.parse().unwrap();
1011 let sol_balanced_market_token = SOL_BALANCED_MARKET_TOKEN.parse().unwrap();
1012 let bnb_balanced_market_token = BNB_BALANCED_MARKET_TOKEN.parse().unwrap();
1013 let aave_balanced_market_token = AAVE_BALANCED_MARKET_TOKEN.parse().unwrap();
1014
1015 let glv_token = Pubkey::new_unique();
1016 let mut glv = Glv::zeroed();
1017 glv.glv_token = glv_token;
1018 glv.long_token = wsol;
1019 glv.short_token = usdc;
1020
1021 for market_token in [
1022 sol_balanced_market_token,
1023 bnb_balanced_market_token,
1024 aave_balanced_market_token,
1025 ] {
1026 glv.markets.insert(
1027 &market_token,
1028 GlvMarketConfig {
1029 max_amount: 10_000 * MARKET_TOKEN_UNIT,
1030 flags: Zeroable::zeroed(),
1031 padding_0: Zeroable::zeroed(),
1032 max_value: 10_000 * MARKET_USD_UNIT,
1033 balance: 1_000 * MARKET_TOKEN_UNIT,
1034 padding_1: Zeroable::zeroed(),
1035 },
1036 );
1037 }
1038
1039 let model = GlvModel::new(Arc::new(glv), 3_000 * MARKET_TOKEN_UNIT);
1040
1041 (glv_token, model)
1042 }
1043
1044 fn create_and_update_market_graph() -> crate::Result<(MarketGraph, HashSet<Pubkey>)> {
1045 let mut graph = MarketGraph::default();
1046 let updates = get_market_updates();
1047 let prices = get_price_updates();
1048 let mut market_tokens = HashSet::<Pubkey>::default();
1049
1050 for (data, supply) in updates {
1052 let market = try_deserialize_zero_copy_from_base64::<Market>(&data)?.0;
1053 market_tokens.insert(market.meta.market_token_mint);
1054 graph.insert_market(MarketModel::from_parts(Arc::new(market), supply));
1055 }
1056
1057 for (_, token, price) in prices {
1059 graph.update_token_price(&token, &price);
1060 }
1061
1062 Ok((graph, market_tokens))
1063 }
1064
1065 #[test]
1066 fn basic() -> crate::Result<()> {
1067 let _tracing = setup_fmt_tracing("info");
1068
1069 let (mut graph, market_tokens) = create_and_update_market_graph()?;
1070
1071 graph.update_value(10 * constants::MARKET_USD_UNIT);
1073 graph.update_base_cost(constants::MARKET_USD_UNIT / 100);
1074
1075 let num_markets = graph.markets().count();
1076 assert_eq!(num_markets, market_tokens.len());
1077 for market_token in market_tokens {
1078 let market = graph.get_market(&market_token).expect("must exist");
1079 assert_eq!(market.meta.market_token_mint, market_token);
1080 }
1081 println!("{:?}", Dot::new(&graph.graph));
1082 Ok(())
1083 }
1084
1085 #[test]
1086 fn best_swap_path() -> crate::Result<()> {
1087 let _tracing = setup_fmt_tracing("info");
1088
1089 let usdc: Pubkey = USDC.parse().unwrap();
1090 let wsol: Pubkey = WSOL.parse().unwrap();
1091 let bome: Pubkey = BOME.parse().unwrap();
1092
1093 let (mut g, _) = create_and_update_market_graph()?;
1094
1095 g.update_value(constants::MARKET_USD_UNIT);
1096
1097 for steps in 0..=5 {
1098 g.update_max_steps(steps);
1099
1100 let paths = g.best_swap_paths(&wsol, false)?;
1101 let dfs_paths = g.best_swap_paths(&wsol, true)?;
1102
1103 let (rate, best_path) = paths.to(&bome);
1104 let (dfs_rate, dfs_best_path) = dfs_paths.to(&bome);
1105 assert_eq!(rate, dfs_rate);
1106 assert_eq!(best_path, dfs_best_path);
1107
1108 let (rate, best_path) = paths.to(&usdc);
1109 let (dfs_rate, dfs_best_path) = dfs_paths.to(&usdc);
1110 assert_eq!(rate, dfs_rate);
1111 assert_eq!(best_path, dfs_best_path);
1112
1113 for target in [&usdc, &wsol, &bome] {
1114 println!("[{steps}] path to {target}: {:?}", paths.to(target));
1115 }
1116 }
1117
1118 Ok(())
1119 }
1120
1121 #[test]
1122 #[cfg(simulation)]
1123 fn position_order_simulation() -> crate::Result<()> {
1124 use crate::simulation::order::OrderSimulationOutput;
1125
1126 let _tracing = setup_fmt_tracing("info");
1127
1128 let bome: Pubkey = BOME.parse().unwrap();
1129 let wsol: Pubkey = WSOL.parse().unwrap();
1130 let market_token: Pubkey = SOL_BALANCED_MARKET_TOKEN.parse().unwrap();
1131
1132 let (mut g, _) = create_and_update_market_graph()?;
1133
1134 g.update_value(constants::MARKET_USD_UNIT * 6);
1135 g.update_max_steps(5);
1136
1137 let paths = g.best_swap_paths(&bome, false)?;
1138 let (_, best_path) = paths.to(&wsol);
1139
1140 println!("{best_path:?}");
1141
1142 let mut simulator = g.to_simulator(Default::default());
1143 let bome_price = 101468850000;
1144 let sol_price = 10821227000000;
1145 let amount = 5 * constants::MARKET_USD_UNIT / bome_price;
1146 let size = 100 * constants::MARKET_USD_UNIT;
1147 let params = CreateOrderParams::builder()
1148 .amount(amount)
1149 .is_long(true)
1150 .size(size)
1151 .market_token(market_token)
1152 .build();
1153 let output = simulator
1154 .simulate_order(CreateOrderKind::MarketIncrease, ¶ms, &wsol)
1155 .pay_token(Some(&bome))
1156 .swap_path(&best_path)
1157 .build()
1158 .execute_with_options(Default::default())?;
1159
1160 println!("{output:?}");
1161
1162 g.update_with_simulator(&simulator, Default::default());
1164 g.update_value(constants::MARKET_USD_UNIT * 5);
1165 let paths = g.best_swap_paths(&wsol, false)?;
1166 let (_, best_path) = paths.to(&bome);
1167 println!("{best_path:?}");
1168
1169 let size = 50 * constants::MARKET_USD_UNIT;
1170 let params = CreateOrderParams::builder()
1171 .amount(amount)
1172 .is_long(true)
1173 .size(size)
1174 .market_token(market_token)
1175 .trigger_price(sol_price * 101 / 100)
1176 .build();
1177 let OrderSimulationOutput::Increase { position, .. } = output else {
1178 unreachable!()
1179 };
1180 let output = simulator
1181 .simulate_order(CreateOrderKind::LimitDecrease, ¶ms, &wsol)
1182 .receive_token(Some(&bome))
1183 .swap_path(&best_path)
1184 .position(Some(position.position_arc()))
1185 .build()
1186 .update_prices(Default::default())?
1187 .execute_with_options(Default::default());
1188
1189 println!("{output:?}");
1190
1191 Ok(())
1192 }
1193
1194 #[test]
1195 #[cfg(simulation)]
1196 fn swap_order_simulation() -> crate::Result<()> {
1197 let _tracing = setup_fmt_tracing("info");
1198
1199 let bome: Pubkey = BOME.parse().unwrap();
1200 let wsol: Pubkey = WSOL.parse().unwrap();
1201 let market_token: Pubkey = SOL_BALANCED_MARKET_TOKEN.parse().unwrap();
1202
1203 let (mut g, _) = create_and_update_market_graph()?;
1204
1205 g.update_value(constants::MARKET_USD_UNIT * 6);
1206 g.update_max_steps(5);
1207
1208 let paths = g.best_swap_paths(&bome, false)?;
1209 let (_, best_path) = paths.to(&wsol);
1210
1211 println!("{best_path:?}");
1212
1213 let bome_price = 101468850000;
1214 let sol_price = 10821227000000;
1215 let amount = 5 * constants::MARKET_USD_UNIT / bome_price;
1216 let params = CreateOrderParams::builder()
1217 .amount(amount)
1218 .is_long(true)
1219 .size(0)
1220 .market_token(market_token)
1221 .build();
1222 let output = g
1223 .to_simulator(Default::default())
1224 .simulate_order(CreateOrderKind::MarketSwap, ¶ms, &wsol)
1225 .pay_token(Some(&bome))
1226 .swap_path(&best_path)
1227 .build()
1228 .execute_with_options(Default::default())?;
1229
1230 println!("{output:?}");
1231
1232 let amount = 5 * constants::MARKET_USD_UNIT / bome_price;
1233 let params = CreateOrderParams::builder()
1234 .amount(amount)
1235 .is_long(true)
1236 .size(0)
1237 .market_token(market_token)
1238 .min_output(amount * bome_price / sol_price * 102 / 100)
1239 .build();
1240 let output = g
1241 .to_simulator(Default::default())
1242 .simulate_order(CreateOrderKind::LimitSwap, ¶ms, &wsol)
1243 .pay_token(Some(&bome))
1244 .swap_path(&best_path)
1245 .build()
1246 .update_prices(Default::default())?
1247 .execute_with_options(Default::default())?;
1248
1249 println!("{output:?}");
1250
1251 Ok(())
1252 }
1253
1254 #[test]
1255 #[cfg(simulation)]
1256 fn deposit_simulation() -> crate::Result<()> {
1257 use gmsol_programs::gmsol_store::types::CreateDepositParams;
1258
1259 let _tracing = setup_fmt_tracing("info");
1260
1261 let bome: Pubkey = BOME.parse().unwrap();
1262 let wsol: Pubkey = WSOL.parse().unwrap();
1263 let usdc: Pubkey = USDC.parse().unwrap();
1264 let market_token: Pubkey = SOL_BALANCED_MARKET_TOKEN.parse().unwrap();
1265
1266 let (mut g, _) = create_and_update_market_graph()?;
1267
1268 g.update_value(constants::MARKET_USD_UNIT * 6);
1269 g.update_max_steps(5);
1270
1271 let paths = g.best_swap_paths(&bome, false)?;
1272 let (_, best_long_path) = paths.to(&wsol);
1273
1274 let paths = g.best_swap_paths(&wsol, false)?;
1275 let (_, best_short_path) = paths.to(&usdc);
1276
1277 println!("{best_long_path:?}");
1278 println!("{best_short_path:?}");
1279
1280 let bome_price = 101468850000;
1281 let sol_price = 10821227000000;
1282 let long_amount = 5 * constants::MARKET_USD_UNIT / bome_price;
1283 let short_amount = 5 * constants::MARKET_USD_UNIT / sol_price;
1284 let params = CreateDepositParams {
1285 execution_lamports: 0,
1286 long_token_swap_length: best_long_path.len() as u8,
1287 short_token_swap_length: best_short_path.len() as u8,
1288 initial_long_token_amount: long_amount as u64,
1289 initial_short_token_amount: short_amount as u64,
1290 min_market_token_amount: 0,
1291 should_unwrap_native_token: true,
1292 };
1293 let output = g
1294 .to_simulator(Default::default())
1295 .simulate_deposit(&market_token, ¶ms)
1296 .long_pay_token(Some(&bome))
1297 .long_swap_path(&best_long_path)
1298 .short_pay_token(Some(&wsol))
1299 .short_swap_path(&best_short_path)
1300 .build()
1301 .execute_with_options(Default::default())?;
1302
1303 println!("{output:?}");
1304
1305 Ok(())
1306 }
1307
1308 #[test]
1309 #[cfg(simulation)]
1310 fn withdrawal_simulation() -> crate::Result<()> {
1311 use gmsol_programs::gmsol_store::types::CreateWithdrawalParams;
1312
1313 let _tracing = setup_fmt_tracing("info");
1314
1315 let bome: Pubkey = BOME.parse().unwrap();
1316 let wsol: Pubkey = WSOL.parse().unwrap();
1317 let usdc: Pubkey = USDC.parse().unwrap();
1318 let market_token: Pubkey = SOL_BALANCED_MARKET_TOKEN.parse().unwrap();
1319
1320 let (mut g, _) = create_and_update_market_graph()?;
1321
1322 g.update_value(constants::MARKET_USD_UNIT * 6);
1323 g.update_max_steps(5);
1324
1325 let paths = g.best_swap_paths(&wsol, false)?;
1326 let (_, best_long_path) = paths.to(&bome);
1327
1328 let paths = g.best_swap_paths(&usdc, false)?;
1329 let (_, best_short_path) = paths.to(&wsol);
1330
1331 println!("{best_long_path:?}");
1332 println!("{best_short_path:?}");
1333
1334 let market_token_amount = 5 * 1_000_000_000;
1335
1336 let params = CreateWithdrawalParams {
1337 execution_lamports: 0,
1338 long_token_swap_path_length: best_long_path.len() as u8,
1339 short_token_swap_path_length: best_short_path.len() as u8,
1340 market_token_amount,
1341 min_long_token_amount: 0,
1342 min_short_token_amount: 0,
1343 should_unwrap_native_token: true,
1344 };
1345 let output = g
1346 .to_simulator(Default::default())
1347 .simulate_withdrawal(&market_token, ¶ms)
1348 .long_receive_token(Some(&bome))
1349 .long_swap_path(&best_long_path)
1350 .short_receive_token(Some(&wsol))
1351 .short_swap_path(&best_short_path)
1352 .build()
1353 .execute_with_options(Default::default())?;
1354
1355 println!("{output:?}");
1356
1357 Ok(())
1358 }
1359
1360 #[test]
1361 #[cfg(simulation)]
1362 fn shift_simulation() -> crate::Result<()> {
1363 use gmsol_programs::gmsol_store::types::CreateShiftParams;
1364
1365 let _tracing = setup_fmt_tracing("info");
1366
1367 let from_market_token: Pubkey = SOL_BALANCED_MARKET_TOKEN.parse().unwrap();
1368 let to_market_token: Pubkey = BNB_BALANCED_MARKET_TOKEN.parse().unwrap();
1369
1370 let (g, _) = create_and_update_market_graph()?;
1371
1372 let from_market_token_amount = 5 * 1_000_000_000;
1373
1374 let params = CreateShiftParams {
1375 execution_lamports: 0,
1376 from_market_token_amount,
1377 min_to_market_token_amount: 0,
1378 };
1379 let output = g
1380 .to_simulator(Default::default())
1381 .simulate_shift(&from_market_token, &to_market_token, ¶ms)
1382 .build()
1383 .execute_with_options(Default::default())?;
1384
1385 println!("{output:?}");
1386
1387 Ok(())
1388 }
1389
1390 #[test]
1391 #[cfg(simulation)]
1392 fn glv_deposit_and_withdrawal_simulation() -> crate::Result<()> {
1393 use gmsol_programs::gmsol_store::types::{
1394 CreateGlvDepositParams, CreateGlvWithdrawalParams,
1395 };
1396
1397 let _tracing = setup_fmt_tracing("info");
1398
1399 let bome: Pubkey = BOME.parse().unwrap();
1400 let wsol: Pubkey = WSOL.parse().unwrap();
1401 let usdc: Pubkey = USDC.parse().unwrap();
1402 let market_token: Pubkey = SOL_BALANCED_MARKET_TOKEN.parse().unwrap();
1403
1404 let (glv_token, glv) = get_glv();
1405
1406 let initial_supply = glv.supply();
1407
1408 let (mut g, _) = create_and_update_market_graph()?;
1409
1410 g.update_value(constants::MARKET_USD_UNIT * 6);
1411 g.update_max_steps(5);
1412
1413 let paths = g.best_swap_paths(&bome, false)?;
1414 let (_, best_long_path) = paths.to(&wsol);
1415
1416 let paths = g.best_swap_paths(&wsol, false)?;
1417 let (_, best_short_path) = paths.to(&usdc);
1418
1419 println!("{best_long_path:?}");
1420 println!("{best_short_path:?}");
1421
1422 let bome_price = 101468850000;
1423 let sol_price = 10821227000000;
1424 let long_amount = 5 * constants::MARKET_USD_UNIT / bome_price;
1425 let short_amount = 5 * constants::MARKET_USD_UNIT / sol_price;
1426 let params = CreateGlvDepositParams {
1427 execution_lamports: 0,
1428 market_token_amount: 10 * 1_000_000_000,
1429 long_token_swap_length: best_long_path.len() as u8,
1430 short_token_swap_length: best_short_path.len() as u8,
1431 initial_long_token_amount: long_amount as u64,
1432 initial_short_token_amount: short_amount as u64,
1433 min_market_token_amount: 0,
1434 min_glv_token_amount: 0,
1435 should_unwrap_native_token: true,
1436 };
1437
1438 let mut simulator = g.to_simulator(Default::default());
1439
1440 simulator.insert_glv(glv);
1441
1442 let output = simulator
1443 .simulate_glv_deposit(&glv_token, &market_token, ¶ms)
1444 .long_pay_token(Some(&bome))
1445 .long_swap_path(&best_long_path)
1446 .short_pay_token(Some(&wsol))
1447 .short_swap_path(&best_short_path)
1448 .build()
1449 .execute_with_options(Default::default())?;
1450
1451 println!("deposit: {output:?}");
1452
1453 g.update_with_simulator(&simulator, Default::default());
1454
1455 let paths = g.best_swap_paths(&wsol, false)?;
1456 let (_, best_long_path) = paths.to(&bome);
1457
1458 let paths = g.best_swap_paths(&usdc, false)?;
1459 let (_, best_short_path) = paths.to(&wsol);
1460
1461 println!("{best_long_path:?}");
1462 println!("{best_short_path:?}");
1463
1464 let params = CreateGlvWithdrawalParams {
1465 execution_lamports: 0,
1466 glv_token_amount: output.output_amount(),
1467 long_token_swap_length: best_long_path.len() as u8,
1468 short_token_swap_length: best_short_path.len() as u8,
1469 min_final_long_token_amount: 0,
1470 min_final_short_token_amount: 0,
1471 should_unwrap_native_token: true,
1472 };
1473
1474 let output = simulator
1475 .simulate_glv_withdrawal(&glv_token, &market_token, ¶ms)
1476 .long_receive_token(Some(&bome))
1477 .long_swap_path(&best_long_path)
1478 .short_receive_token(Some(&wsol))
1479 .short_swap_path(&best_short_path)
1480 .build()
1481 .execute_with_options(Default::default())?;
1482
1483 println!("withdrawal: {output:?}");
1484
1485 let glv = simulator.get_glv(&glv_token).unwrap();
1486 assert_eq!(glv.supply(), initial_supply);
1487
1488 Ok(())
1489 }
1490}