Skip to main content

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}