bothan_lib/registry/source.rs
1// ! Source definitions and routing operations for signal computation.
2//!
3//! This module provides structures for defining data sources and routing operations
4//! that transform source data before it is processed. It enables the flexible
5//! combination and transformation of data from various sources.
6//!
7//! The module provides:
8//!
9//! - The [`Operation`] enum which defines basic arithmetic operations
10//! - The [`OperationRoute`] struct which pairs operations with dependent signals
11//! - The [`SourceQuery`] struct which specifies where to obtain input data
12//!
13//! # Source Routing
14//!
15//! Sources can be configured with routes that apply transformations using values
16//! from other signals. This enables complex relationships between signals, such as:
17//!
18//! - Converting between different units (e.g., USDT to USD)
19//! - Adjusting values using scaling factors
20//! - Applying corrections based on other market data
21//!
22//! Routing operations are applied sequentially, with each operation using the
23//! result of the previous operation as its first operand.
24
25use bincode::{Decode, Encode};
26use num_traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub};
27use serde::{Deserialize, Serialize};
28
29/// Arithmetic operations that can be applied in routing transformations.
30///
31/// The `Operation` enum represents the four basic arithmetic operations that
32/// can be used to transform values during the routing process. Each operation
33/// is designed to safely handle potential numerical errors using checked operations.
34///
35/// # Variants
36///
37/// * `Add` - Addition operation (+)
38/// * `Subtract` - Subtraction operation (-)
39/// * `Multiply` - Multiplication operation (*)
40/// * `Divide` - Division operation (/)
41///
42/// # Examples
43///
44/// ```
45/// use bothan_lib::registry::source::Operation;
46/// use rust_decimal::Decimal;
47///
48/// // Create a multiplication operation
49/// let operation = Operation::Multiply;
50///
51/// // Apply the operation to two values
52/// let a = Decimal::new(10, 0); // 10.0
53/// let b = Decimal::new(15, 0); // 15.0
54/// let result = operation.execute(a, b).unwrap();
55///
56/// assert_eq!(result, Decimal::new(150, 0)); // 150.0
57/// ```
58#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
59pub enum Operation {
60 /// Addition operation (+)
61 #[serde(rename = "+")]
62 Add,
63 /// Subtraction operation (-)
64 #[serde(rename = "-")]
65 Subtract,
66 /// Multiplication operation (*)
67 #[serde(rename = "*")]
68 Multiply,
69 /// Division operation (/)
70 #[serde(rename = "/")]
71 Divide,
72}
73
74impl Operation {
75 /// Executes the arithmetic operation on two values.
76 ///
77 /// This method applies the operation represented by the enum variant to the
78 /// provided operands. It uses checked operations to prevent numerical errors
79 /// such as overflow or division by zero.
80 ///
81 /// # Examples
82 ///
83 /// ```
84 /// use bothan_lib::registry::source::Operation;
85 ///
86 /// let add_op = Operation::Add;
87 /// assert_eq!(add_op.execute(5, 3), Some(8));
88 ///
89 /// let div_op = Operation::Divide;
90 /// assert_eq!(div_op.execute(10, 2), Some(5));
91 /// assert_eq!(div_op.execute(10, 0), None); // Division by zero returns None
92 /// ```
93 pub fn execute<T>(&self, a: T, b: T) -> Option<T>
94 where
95 T: CheckedAdd + CheckedSub + CheckedMul + CheckedDiv + Copy,
96 {
97 match self {
98 Operation::Add => a.checked_add(&b),
99 Operation::Subtract => a.checked_sub(&b),
100 Operation::Multiply => a.checked_mul(&b),
101 Operation::Divide => a.checked_div(&b),
102 }
103 }
104}
105
106/// A transformation that applies an operation using a value from another signal.
107///
108/// The `OperationRoute` struct defines a step in a routing transformation chain.
109/// It specifies a signal whose value should be used as the second operand in
110/// an operation, with the first operand being either the original source value
111/// or the result of previous routing operations.
112///
113/// Routes are applied sequentially, allowing for complex transformations to be
114/// built from simple arithmetic operations.
115///
116/// # Examples
117///
118/// ```
119/// use bothan_lib::registry::source::{OperationRoute, Operation};
120///
121/// // Create a route that multiplies by the "USDT-USD" signal's value
122/// let route = OperationRoute::new("USDT-USD", Operation::Multiply);
123/// ```
124#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
125pub struct OperationRoute {
126 /// The identifier of the signal whose value will be used in the operation.
127 ///
128 /// When this route is applied, the system will look up the current value
129 /// of the signal with this ID and use it as the second operand in the operation.
130 pub signal_id: String,
131
132 /// The arithmetic operation to apply.
133 ///
134 /// This defines how the current value will be combined with the value
135 /// from the referenced signal.
136 pub operation: Operation,
137}
138
139impl OperationRoute {
140 /// Creates a new operation route.
141 ///
142 /// This constructor creates an operation route that will apply the specified
143 /// operation using the value of the referenced signal.
144 ///
145 /// # Examples
146 ///
147 /// ```
148 /// use bothan_lib::registry::source::{OperationRoute, Operation};
149 ///
150 /// // Create a route that divides by the "USD-EUR" signal's value
151 /// let route = OperationRoute::new("USD-EUR", Operation::Divide);
152 /// ```
153 pub fn new<T: Into<String>>(signal_id: T, operation: Operation) -> Self {
154 OperationRoute {
155 signal_id: signal_id.into(),
156 operation,
157 }
158 }
159}
160
161/// A specification for retrieving and transforming data from a source.
162///
163/// The `SourceQuery` struct defines where to obtain data and how to transform it
164/// before processing. It specifies a data source, an identifier to query within
165/// that source, and optionally a series of routing operations to apply to the
166/// retrieved value.
167///
168/// This structure is a key component of signal definitions, allowing each signal
169/// to combine and transform data from multiple sources.
170///
171/// # Examples
172///
173/// ```
174/// use bothan_lib::registry::source::{SourceQuery, OperationRoute, Operation};
175///
176/// // Create a query for BTC/USDT from Binance, converted to USD
177/// let query = SourceQuery::new(
178/// "binance",
179/// "btcusdt",
180/// vec![
181/// // Convert from USDT to USD using the USDT-USD signal
182/// OperationRoute::new("USDT-USD", Operation::Multiply),
183/// ]
184/// );
185/// ```
186#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
187pub struct SourceQuery {
188 /// The identifier of the data source.
189 ///
190 /// This corresponds to a registered worker that can provide data for this query.
191 /// Examples might include exchange names like "binance" or "coinbase".
192 pub source_id: String,
193
194 /// The query identifier to use within the data source.
195 ///
196 /// This specifies what data to retrieve from the source, such as a trading pair
197 /// like "btcusdt" or an asset identifier like "bitcoin".
198 #[serde(rename = "id")]
199 pub query_id: String,
200
201 /// Routing operations to apply to the retrieved value.
202 ///
203 /// These operations are applied sequentially to transform the source value
204 /// before it is sent to the processor. If not provided, no transformations
205 /// will be applied.
206 #[serde(default)]
207 #[serde(skip_serializing_if = "Vec::is_empty")]
208 pub routes: Vec<OperationRoute>,
209}
210
211impl SourceQuery {
212 /// Creates a new source query.
213 ///
214 /// This constructor creates a source query that will retrieve data from the
215 /// specified source and apply the provided routing operations.
216 ///
217 /// # Examples
218 ///
219 /// ```
220 /// use bothan_lib::registry::source::{SourceQuery, OperationRoute, Operation};
221 ///
222 /// // Create a query for ETH/USD from Coinbase with no transformations
223 /// let direct_query = SourceQuery::new("coinbase", "ETH-USD", vec![]);
224 ///
225 /// // Create a query for ETH/BTC from Kraken, converted to USD
226 /// let converted_query = SourceQuery::new(
227 /// "kraken",
228 /// "ethbtc",
229 /// vec![
230 /// // Convert from BTC to USD using the BTC-USD signal
231 /// OperationRoute::new("BTC-USD", Operation::Multiply),
232 /// ]
233 /// );
234 /// ```
235 pub fn new<T, U>(source_id: T, query_id: U, routes: Vec<OperationRoute>) -> Self
236 where
237 T: Into<String>,
238 U: Into<String>,
239 {
240 SourceQuery {
241 source_id: source_id.into(),
242 query_id: query_id.into(),
243 routes,
244 }
245 }
246}