Skip to content

Commit 99466da

Browse files
feat: add analysis tools
1 parent fe8935f commit 99466da

File tree

2 files changed

+552
-6
lines changed

2 files changed

+552
-6
lines changed

analysis-tools/src/arbitrage.rs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
//! # Arbitrage Module
2+
//!
3+
//! This module provides tools for detecting arbitrage opportunities in cryptocurrency markets.
4+
//! It includes functionalities for identifying simple, triangular, and more complex arbitrage
5+
//! scenarios.
6+
7+
use aggregator_core::{ArbitrageOpportunity, Summary, TradingPair};
8+
use chrono::Utc;
9+
use std::collections::HashMap;
10+
11+
/// # Arbitrage Detector
12+
///
13+
/// A struct that encapsulates the logic for detecting arbitrage opportunities. It holds
14+
/// configurable thresholds for minimum profit and volume to filter out insignificant
15+
/// opportunities.
16+
///
17+
/// ## Fields
18+
///
19+
/// - `min_profit_threshold`: The minimum profit percentage required to consider an
20+
/// opportunity as valid.
21+
/// - `min_volume_threshold`: The minimum trade volume required for an opportunity.
22+
pub struct ArbitrageDetector {
23+
min_profit_threshold: f64,
24+
min_volume_threshold: f64,
25+
}
26+
27+
impl ArbitrageDetector {
28+
/// ## New
29+
///
30+
/// Creates a new `ArbitrageDetector` with specified profit and volume thresholds.
31+
///
32+
/// ### Arguments
33+
///
34+
/// - `min_profit_threshold`: A `f64` representing the minimum profit percentage.
35+
/// - `min_volume_threshold`: A `f64` representing the minimum trade volume.
36+
pub fn new(min_profit_threshold: f64, min_volume_threshold: f64) -> Self {
37+
Self {
38+
min_profit_threshold,
39+
min_volume_threshold,
40+
}
41+
}
42+
43+
/// ## Detect Opportunities
44+
///
45+
/// Detects simple arbitrage opportunities by comparing the best bid and ask prices across
46+
/// multiple exchanges for a given trading pair.
47+
///
48+
/// ### Arguments
49+
///
50+
/// - `summaries`: A `HashMap` where the key is a `TradingPair` and the value is a `Vec`
51+
/// of `Summary` objects from different exchanges.
52+
///
53+
/// ### Returns
54+
///
55+
/// A `Vec` of `ArbitrageOpportunity` structs, each representing a profitable arbitrage
56+
/// opportunity.
57+
pub async fn detect_opportunities(
58+
&self,
59+
summaries: &HashMap<TradingPair, Vec<Summary>>,
60+
) -> Vec<ArbitrageOpportunity> {
61+
let mut opportunities = Vec::new();
62+
63+
for (pair, exchange_summaries) in summaries {
64+
if exchange_summaries.len() < 2 {
65+
continue; // Need at least 2 exchanges for arbitrage
66+
}
67+
68+
// Find the best bid and ask across all exchanges
69+
let mut best_bid: Option<(&Summary, f64)> = None;
70+
let mut best_ask: Option<(&Summary, f64)> = None;
71+
72+
for summary in exchange_summaries {
73+
if let Some(bid) = summary.bids.first() {
74+
if best_bid.is_none() || bid.price > best_bid.unwrap().1 {
75+
best_bid = Some((summary, bid.price));
76+
}
77+
}
78+
79+
if let Some(ask) = summary.asks.first() {
80+
if best_ask.is_none() || ask.price < best_ask.unwrap().1 {
81+
best_ask = Some((summary, ask.price));
82+
}
83+
}
84+
}
85+
86+
// Check if there's an arbitrage opportunity
87+
if let (Some((bid_summary, bid_price)), Some((ask_summary, ask_price))) =
88+
(best_bid, best_ask)
89+
{
90+
if bid_price > ask_price {
91+
let profit = bid_price - ask_price;
92+
let profit_percentage = (profit / ask_price) * 100.0;
93+
94+
if profit_percentage >= self.min_profit_threshold {
95+
// Calculate available volume
96+
let bid_volume =
97+
bid_summary.bids.first().map(|b| b.quantity).unwrap_or(0.0);
98+
let ask_volume =
99+
ask_summary.asks.first().map(|a| a.quantity).unwrap_or(0.0);
100+
let available_volume = bid_volume.min(ask_volume);
101+
102+
if available_volume >= self.min_volume_threshold {
103+
opportunities.push(ArbitrageOpportunity {
104+
buy_exchange: ask_summary.asks.first().unwrap().exchange.clone(),
105+
sell_exchange: bid_summary.bids.first().unwrap().exchange.clone(),
106+
symbol: pair.to_string(),
107+
buy_price: ask_price,
108+
sell_price: bid_price,
109+
profit_percentage,
110+
volume: available_volume,
111+
timestamp: Utc::now(),
112+
});
113+
}
114+
}
115+
}
116+
}
117+
}
118+
119+
opportunities
120+
}
121+
122+
/// ## Detect Triangular Arbitrage
123+
///
124+
/// Placeholder for detecting triangular arbitrage opportunities. This involves finding
125+
/// price discrepancies between three different trading pairs.
126+
///
127+
/// ### Arguments
128+
///
129+
/// - `_summaries`: A `HashMap` of market summaries.
130+
///
131+
/// ### Returns
132+
///
133+
/// An empty `Vec` as this feature is not yet implemented.
134+
pub async fn detect_triangular_arbitrage(
135+
&self,
136+
_summaries: &HashMap<TradingPair, Vec<Summary>>,
137+
) -> Vec<ArbitrageOpportunity> {
138+
// TODO: Implement triangular arbitrage detection
139+
// This would look for opportunities like BTC/USD -> ETH/BTC -> USD/ETH
140+
vec![]
141+
}
142+
143+
/// ## Detect Negative Cycles
144+
///
145+
/// Placeholder for detecting arbitrage opportunities using the Bellman-Ford algorithm
146+
/// to find negative cycles in the exchange graph.
147+
///
148+
/// ### Arguments
149+
///
150+
/// - `_summaries`: A `HashMap` of market summaries.
151+
///
152+
/// ### Returns
153+
///
154+
/// An empty `Vec` as this feature is not yet implemented.
155+
pub async fn detect_negative_cycles(
156+
&self,
157+
_summaries: &HashMap<TradingPair, Vec<Summary>>,
158+
) -> Vec<ArbitrageOpportunity> {
159+
// TODO: Implement negative cycle detection using Bellman-Ford algorithm
160+
// This is useful for detecting complex arbitrage paths
161+
vec![]
162+
}
163+
}
164+
165+
impl Default for ArbitrageDetector {
166+
fn default() -> Self {
167+
Self::new(0.1, 0.01) // 0.1% profit threshold, 0.01 volume threshold
168+
}
169+
}
170+
171+
#[cfg(test)]
172+
mod tests {
173+
use super::*;
174+
use aggregator_core::{Exchange, PriceLevel};
175+
176+
#[tokio::test]
177+
async fn test_simple_arbitrage_detection() {
178+
let detector = ArbitrageDetector::new(0.1, 0.01);
179+
let mut summaries = HashMap::new();
180+
181+
let pair = TradingPair::new("BTC", "USDT");
182+
183+
// Create mock summaries with arbitrage opportunity
184+
let summary1 = Summary {
185+
symbol: "BTCUSDT".to_string(),
186+
spread: 1.0,
187+
bids: vec![PriceLevel {
188+
price: 50000.0,
189+
quantity: 1.0,
190+
exchange: Exchange::Binance,
191+
timestamp: Utc::now(),
192+
}],
193+
asks: vec![PriceLevel {
194+
price: 50001.0,
195+
quantity: 1.0,
196+
exchange: Exchange::Binance,
197+
timestamp: Utc::now(),
198+
}],
199+
timestamp: Utc::now(),
200+
};
201+
202+
let summary2 = Summary {
203+
symbol: "BTCUSDT".to_string(),
204+
spread: 1.0,
205+
bids: vec![PriceLevel {
206+
price: 49900.0,
207+
quantity: 1.0,
208+
exchange: Exchange::Bybit,
209+
timestamp: Utc::now(),
210+
}],
211+
asks: vec![PriceLevel {
212+
price: 49950.0, // Lower ask price creates arbitrage opportunity
213+
quantity: 1.0,
214+
exchange: Exchange::Bybit,
215+
timestamp: Utc::now(),
216+
}],
217+
timestamp: Utc::now(),
218+
};
219+
220+
summaries.insert(pair, vec![summary1, summary2]);
221+
222+
let opportunities = detector.detect_opportunities(&summaries).await;
223+
assert!(!opportunities.is_empty());
224+
225+
let opportunity = &opportunities[0];
226+
assert_eq!(opportunity.buy_exchange, Exchange::Bybit);
227+
assert_eq!(opportunity.sell_exchange, Exchange::Binance);
228+
assert!(opportunity.profit_percentage > 0.1);
229+
}
230+
}

0 commit comments

Comments
 (0)