fynd_core/graph/mod.rs
1//! Graph management for algorithms.
2//!
3//! This module provides the GraphManager trait which solvers use to manage their market graph
4//! representation. GraphManager handles both building graphs from market data and updating them
5//! based on market events.
6
7pub mod petgraph;
8pub mod token_graph;
9
10pub use petgraph::{EdgeData, PetgraphStableDiGraphManager, StableDiGraph};
11use rustc_hash::{FxHashMap, FxHashSet};
12use smallvec::SmallVec;
13use thiserror::Error;
14pub use token_graph::{PairEdge, TokenGraph, TokenPath, TopologyGraph, TopologyGraphManager};
15use tycho_simulation::{
16 tycho_common::{models::Address, simulation::protocol_sim::ProtocolSim},
17 tycho_core::models::token::Token,
18};
19
20use crate::{derived::DerivedData, feed::market_data::MarketDataView, types::ComponentId};
21
22/// Tokens held without allocating. A path of `h` hops names `h + 1` tokens, so this covers every
23/// `max_hops` up to 4. A deeper path still works: `SmallVec` moves to the heap and behaves as a
24/// `Vec` from there.
25pub(crate) const INLINE_TOKENS: usize = 5;
26
27/// Edges held without allocating. A path's edges are its tokens less one, and an edge is a leg of
28/// a route, so this sizes per-leg buffers too.
29pub(crate) const INLINE_EDGES: usize = INLINE_TOKENS - 1;
30
31/// A route with a pool chosen for every leg.
32///
33/// Borrows from the graph rather than copying it, so scoring and simulation read a leg's component
34/// id and weight without a lookup.
35#[derive(Default)]
36pub struct Path<'a, D> {
37 /// The tokens the route passes through, in order.
38 pub tokens: SmallVec<[&'a Address; INLINE_TOKENS]>,
39 /// The pool taken on each leg. One shorter than `tokens`.
40 pub edge_data: SmallVec<[&'a EdgeData<D>; INLINE_EDGES]>,
41}
42
43/// Written out rather than derived so the copy is a `memcpy`: `SmallVec` takes that path only
44/// through `from_slice`, which the derived `Clone` cannot call.
45impl<D> Clone for Path<'_, D> {
46 fn clone(&self) -> Self {
47 Self {
48 tokens: SmallVec::from_slice(&self.tokens),
49 edge_data: SmallVec::from_slice(&self.edge_data),
50 }
51 }
52}
53
54impl<'a, D> Path<'a, D> {
55 /// Creates a new empty Path.
56 pub fn new() -> Self {
57 Self { tokens: SmallVec::new(), edge_data: SmallVec::new() }
58 }
59
60 /// Adds a hop to the path.
61 ///
62 /// Arguments:
63 /// - from: The starting token address of the hop.
64 /// - edge_data: The edge data for the hop.
65 /// - to: The ending token address of the hop.
66 pub fn add_hop(&mut self, from: &'a Address, edge_data: &'a EdgeData<D>, to: &'a Address) {
67 if self.tokens.is_empty() {
68 self.tokens.push(from);
69 }
70 self.tokens.push(to);
71 self.edge_data.push(edge_data);
72 }
73
74 /// Returns the number of hops in the path.
75 pub fn len(&self) -> usize {
76 self.edge_data.len()
77 }
78
79 /// Returns true if the path has no hops.
80 pub fn is_empty(&self) -> bool {
81 self.edge_data.is_empty()
82 }
83
84 /// Returns an iterator over the edges in the path.
85 pub fn edge_iter(&self) -> &[&'a EdgeData<D>] {
86 &self.edge_data
87 }
88
89 /// Returns an iterator over hops in the path (from_token, edge_data, to_token).
90 pub fn iter(&self) -> impl Iterator<Item = (&'a Address, &'a EdgeData<D>, &'a Address)> + '_ {
91 self.tokens
92 .windows(2)
93 .zip(self.edge_data.iter())
94 .map(|(tokens, edge)| (tokens[0], *edge, tokens[1]))
95 }
96
97 /// Creates a new reversed Path from the current one.
98 pub fn reversed(self) -> Self {
99 let reversed_tokens = self.tokens.into_iter().rev().collect();
100 let reversed_edge_data = self
101 .edge_data
102 .into_iter()
103 .rev()
104 .collect();
105 Self { tokens: reversed_tokens, edge_data: reversed_edge_data }
106 }
107}
108
109/// Errors that can occur during graph operations.
110#[derive(Error, Debug)]
111pub enum GraphError {
112 /// Token address not found as a node in the graph.
113 #[error("Token not found in graph: {0:?}")]
114 TokenNotFound(Address),
115 /// One or more components were not found in the graph.
116 #[error("Components not found in graph: {0:?}")]
117 ComponentsNotFound(Vec<ComponentId>),
118 /// Components with fewer than 2 tokens cannot form edges.
119 #[error("Components with less then 2 tokens cannot be added: {0:?}")]
120 InvalidComponents(Vec<ComponentId>),
121 /// No edge exists between the given tokens for this component (test-only).
122 #[cfg(test)]
123 #[error("No edge found between tokens {0:?} and {1:?} for component {2}")]
124 MissingComponentBetweenTokens(Address, Address, ComponentId),
125}
126
127/// Trait for managing graph representations.
128///
129/// Graph managers are stateful - they maintain the graph internally and update it based on market
130/// events.
131pub trait GraphManager<G>: Send + Sync
132where
133 G: Send + Sync,
134{
135 /// Initializes the graph from the market topology.
136 ///
137 /// Arguments:
138 /// - components: A map of component IDs to their tokens addresses.
139 fn initialize_graph(&mut self, components: &FxHashMap<ComponentId, Vec<Address>>);
140
141 /// Returns a reference to the managed graph.
142 fn graph(&self) -> &G;
143}
144
145/// What a caller will accept from a route search.
146///
147/// Bundles the bounds every path query carries so they travel as one argument instead of three.
148pub struct GraphQueryFilter {
149 /// Shortest route to return, in hops. A query with `0` matches nothing.
150 pub min_hops: usize,
151 /// Longest route to return, in hops.
152 pub max_hops: usize,
153 /// Tokens a route may pass *through*. Its own endpoints are always allowed, whatever this
154 /// holds. `None` allows every token.
155 pub connector_tokens: Option<FxHashSet<Address>>,
156}
157
158/// Trait for edge weight types that can be computed from a ProtocolSim and DerivedData.
159///
160/// Implement this trait for edge data types that should use pre-computed derived data
161/// (component depths, spot prices, etc.) instead of computing them from scratch.
162pub trait EdgeWeightFromSimAndDerived: Sized {
163 /// Computes edge weight data using ProtocolSim and pre-computed DerivedData.
164 ///
165 /// # Arguments
166 ///
167 /// * `sim` - The protocol simulation state
168 /// * `component_id` - The component ID for derived data lookup
169 /// * `token_in` - The input token
170 /// * `token_out` - The output token
171 /// * `derived` - Pre-computed derived data (component depths, spot prices, etc.)
172 ///
173 /// # Returns
174 ///
175 /// The computed edge weight, or `None` if it cannot be computed.
176 fn from_sim_and_derived(
177 sim: &dyn ProtocolSim,
178 component_id: &ComponentId,
179 token_in: &Token,
180 token_out: &Token,
181 derived: &DerivedData,
182 ) -> Option<Self>;
183}
184
185/// Trivial implementation for algorithms that don't use edge weights (e.g., Bellman-Ford).
186impl EdgeWeightFromSimAndDerived for () {
187 fn from_sim_and_derived(
188 _sim: &dyn ProtocolSim,
189 _component_id: &ComponentId,
190 _token_in: &Token,
191 _token_out: &Token,
192 _derived: &DerivedData,
193 ) -> Option<Self> {
194 Some(())
195 }
196}
197
198/// Trait for graph managers that support edge weight updates with derived data.
199pub trait EdgeWeightUpdaterWithDerived {
200 /// Updates edge weights using simulation states and pre-computed derived data.
201 ///
202 /// Returns the number of edges successfully updated.
203 fn update_edge_weights_with_derived(
204 &mut self,
205 market: MarketDataView<'_>,
206 derived: &DerivedData,
207 ) -> usize;
208}