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::{
21 derived::DerivedData,
22 feed::market_data::MarketDataView,
23 types::{ComponentId, RouteExclusions},
24};
25
26/// Tokens held without allocating. A path of `h` hops names `h + 1` tokens, so this covers every
27/// `max_hops` up to 4. A deeper path still works: `SmallVec` moves to the heap and behaves as a
28/// `Vec` from there.
29pub(crate) const INLINE_TOKENS: usize = 5;
30
31/// Edges held without allocating. A path's edges are its tokens less one, and an edge is a leg of
32/// a route, so this sizes per-leg buffers too.
33/// Edges a path holds inline before it spills to the heap. One fewer than its tokens.
34pub const INLINE_EDGES: usize = INLINE_TOKENS - 1;
35
36/// A route with a pool chosen for every leg.
37///
38/// Borrows from the graph rather than copying it, so scoring and simulation read a leg's component
39/// id and weight without a lookup.
40#[derive(Default)]
41pub struct Path<'a, D> {
42 /// The tokens the route passes through, in order.
43 pub tokens: SmallVec<[&'a Address; INLINE_TOKENS]>,
44 /// The pool taken on each leg. One shorter than `tokens`.
45 pub edge_data: SmallVec<[&'a EdgeData<D>; INLINE_EDGES]>,
46}
47
48/// Written out rather than derived so the copy is a `memcpy`: `SmallVec` takes that path only
49/// through `from_slice`, which the derived `Clone` cannot call.
50impl<D> Clone for Path<'_, D> {
51 fn clone(&self) -> Self {
52 Self {
53 tokens: SmallVec::from_slice(&self.tokens),
54 edge_data: SmallVec::from_slice(&self.edge_data),
55 }
56 }
57}
58
59impl<'a, D> Path<'a, D> {
60 /// Creates a new empty Path.
61 pub fn new() -> Self {
62 Self { tokens: SmallVec::new(), edge_data: SmallVec::new() }
63 }
64
65 /// Adds a hop to the path.
66 ///
67 /// Arguments:
68 /// - from: The starting token address of the hop.
69 /// - edge_data: The edge data for the hop.
70 /// - to: The ending token address of the hop.
71 pub fn add_hop(&mut self, from: &'a Address, edge_data: &'a EdgeData<D>, to: &'a Address) {
72 if self.tokens.is_empty() {
73 self.tokens.push(from);
74 }
75 self.tokens.push(to);
76 self.edge_data.push(edge_data);
77 }
78
79 /// Returns the number of hops in the path.
80 pub fn len(&self) -> usize {
81 self.edge_data.len()
82 }
83
84 /// Returns true if the path has no hops.
85 pub fn is_empty(&self) -> bool {
86 self.edge_data.is_empty()
87 }
88
89 /// Returns an iterator over the edges in the path.
90 pub fn edge_iter(&self) -> &[&'a EdgeData<D>] {
91 &self.edge_data
92 }
93
94 /// Returns an iterator over hops in the path (from_token, edge_data, to_token).
95 pub fn iter(&self) -> impl Iterator<Item = (&'a Address, &'a EdgeData<D>, &'a Address)> + '_ {
96 self.tokens
97 .windows(2)
98 .zip(self.edge_data.iter())
99 .map(|(tokens, edge)| (tokens[0], *edge, tokens[1]))
100 }
101}
102
103/// Errors that can occur during graph operations.
104#[derive(Error, Debug)]
105pub enum GraphError {
106 /// Token address not found as a node in the graph.
107 #[error("Token not found in graph: {0:?}")]
108 TokenNotFound(Address),
109 /// One or more components were not found in the graph.
110 #[error("Components not found in graph: {0:?}")]
111 ComponentsNotFound(Vec<ComponentId>),
112 /// Components with fewer than 2 tokens cannot form edges.
113 #[error("Components with less then 2 tokens cannot be added: {0:?}")]
114 InvalidComponents(Vec<ComponentId>),
115 /// No edge exists between the given tokens for this component (test-only).
116 #[cfg(any(test, feature = "test-utils"))]
117 #[error("No edge found between tokens {0:?} and {1:?} for component {2}")]
118 MissingComponentBetweenTokens(Address, Address, ComponentId),
119}
120
121/// Trait for managing graph representations.
122///
123/// Graph managers are stateful - they maintain the graph internally and update it based on market
124/// events.
125pub trait GraphManager<G>: Send + Sync
126where
127 G: Send + Sync,
128{
129 /// Initializes the graph from the market topology.
130 ///
131 /// Arguments:
132 /// - components: A map of component IDs to their tokens addresses.
133 fn initialize_graph(&mut self, components: &FxHashMap<ComponentId, Vec<Address>>);
134
135 /// Returns a reference to the managed graph.
136 fn graph(&self) -> &G;
137}
138
139/// The bounds a route search runs under.
140///
141/// Bundles what an algorithm configures once — how long a route may be, and which tokens it may
142/// pass through — so they travel as one argument instead of three. What a request excludes is not
143/// here: that changes per solve and travels beside this as a [`RouteExclusions`].
144#[derive(Debug, Clone)]
145pub struct GraphQueryFilter {
146 /// Shortest route to return, in hops. A query with `0` matches nothing.
147 pub min_hops: usize,
148 /// Longest route to return, in hops.
149 pub max_hops: usize,
150 /// Tokens a route may pass *through*. Its own endpoints are always allowed, whatever this
151 /// holds. `None` allows every token.
152 pub connector_tokens: Option<FxHashSet<Address>>,
153}
154
155/// One solve's route search: the bounds the algorithm configured, and what the request excludes.
156///
157/// The two have different lifetimes — the bounds are built with the algorithm, the exclusions
158/// arrive with the order — so a search borrows both rather than owning either.
159#[derive(Debug, Clone, Copy)]
160pub struct RouteSearch<'a> {
161 /// How long a route may be, and which tokens it may pass through.
162 pub bounds: &'a GraphQueryFilter,
163 /// Pools and tokens this request excludes.
164 pub exclusions: &'a RouteExclusions,
165}
166
167impl RouteSearch<'_> {
168 /// Whether a route may pass through this token: an endpoint always may, and an
169 /// intermediate must clear both the connector list and the request's exclusions.
170 #[must_use]
171 pub fn allows_token(self, token: &Address, endpoints: (&Address, &Address)) -> bool {
172 self.exclusions
173 .allows_token(token, endpoints) &&
174 (token == endpoints.0 ||
175 token == endpoints.1 ||
176 self.bounds
177 .connector_tokens
178 .as_ref()
179 .is_none_or(|tokens| tokens.contains(token)))
180 }
181}
182
183/// Trait for edge weight types that can be computed from a ProtocolSim and DerivedData.
184///
185/// Implement this trait for edge data types that should use pre-computed derived data
186/// (component depths, spot prices, etc.) instead of computing them from scratch.
187pub trait EdgeWeightFromSimAndDerived: Sized {
188 /// Computes edge weight data using ProtocolSim and pre-computed DerivedData.
189 ///
190 /// # Arguments
191 ///
192 /// * `sim` - The protocol simulation state
193 /// * `component_id` - The component ID for derived data lookup
194 /// * `token_in` - The input token
195 /// * `token_out` - The output token
196 /// * `derived` - Pre-computed derived data (component depths, spot prices, etc.)
197 ///
198 /// # Returns
199 ///
200 /// The computed edge weight, or `None` if it cannot be computed.
201 fn from_sim_and_derived(
202 sim: &dyn ProtocolSim,
203 component_id: &ComponentId,
204 token_in: &Token,
205 token_out: &Token,
206 derived: &DerivedData,
207 ) -> Option<Self>;
208}
209
210/// Trivial implementation for algorithms that don't use edge weights (e.g., Bellman-Ford).
211impl EdgeWeightFromSimAndDerived for () {
212 fn from_sim_and_derived(
213 _sim: &dyn ProtocolSim,
214 _component_id: &ComponentId,
215 _token_in: &Token,
216 _token_out: &Token,
217 _derived: &DerivedData,
218 ) -> Option<Self> {
219 Some(())
220 }
221}
222
223/// Trait for graph managers that support edge weight updates with derived data.
224pub trait EdgeWeightUpdaterWithDerived {
225 /// Updates edge weights using simulation states and pre-computed derived data.
226 ///
227 /// Returns the number of edges successfully updated.
228 fn update_edge_weights_with_derived(
229 &mut self,
230 market: MarketDataView<'_>,
231 derived: &DerivedData,
232 ) -> usize;
233}