use crate::{
market_data::order_book::{
Book,
BookConfig,
},
order_book::Side,
};
use anyhow::{
Result,
anyhow,
};
use fuels::types::{
AssetId,
Identity,
};
use std::{
fs::{
self,
File,
},
io::Read,
path::PathBuf,
};
#[derive(Debug, Clone)]
pub struct OrderData {
pub price: u64,
pub quantity: u64,
pub side: Side,
pub trader_id: Identity,
}
pub struct MarketData {
cache_dir: PathBuf,
}
#[derive(Debug, Clone)]
pub enum OrderBookAction {
CreateOrder(OrderData),
CancelOrder(u64),
}
pub mod order_book;
impl MarketData {
pub fn new() -> Result<Self> {
let cache_dir = std::env::temp_dir().join("binance_market_data_cache");
fs::create_dir_all(&cache_dir)?;
Ok(Self { cache_dir })
}
pub async fn download_market_data(&self, pair: &str, date: &str) -> Result<PathBuf> {
let csv_path = self.cache_dir.join(format!("{pair}-trades-{date}.csv"));
if csv_path.exists() {
println!("Data already downloaded");
return Ok(csv_path);
}
let url = format!(
"https://data.binance.vision/data/spot/daily/trades/{pair}/{pair}-trades-{date}.zip"
);
let response = reqwest::get(&url).await?;
if !response.status().is_success() {
return Err(anyhow!("Failed to download: {}", response.status()));
}
let bytes = response.bytes().await?;
let reader = std::io::Cursor::new(bytes);
let mut archive = zip::ZipArchive::new(reader)?;
if archive.len() != 1 {
return Err(anyhow!("Expected exactly one file in the archive"));
}
let mut zip_file = archive.by_index(0)?;
let mut csv_data = Vec::new();
zip_file.read_to_end(&mut csv_data)?;
fs::write(&csv_path, csv_data)?;
Ok(csv_path)
}
pub fn get_order_data(
&self,
pair: &str,
date: &str,
limit: usize,
) -> Result<Vec<OrderData>> {
let csv_path = self.cache_dir.join(format!("{pair}-trades-{date}.csv"));
if !csv_path.exists() {
return Err(anyhow!(
"Data not downloaded. Call download_market_data first."
));
}
let file = File::open(csv_path)?;
let mut rdr = csv::Reader::from_reader(file);
let mut orders = Vec::new();
for (idx, result) in rdr.records().enumerate() {
if idx >= limit {
break;
}
let record = result?;
let price: f64 = record
.get(1)
.ok_or_else(|| anyhow!("Missing price field"))?
.parse()?;
let quantity: f64 = record
.get(2)
.ok_or_else(|| anyhow!("Missing quantity field"))?
.parse()?;
let is_buyer_maker_str = record
.get(5)
.ok_or_else(|| anyhow!("Missing is_buyer_maker field"))?;
let is_buyer_maker = is_buyer_maker_str.to_lowercase() == "true";
let price_u64 = (price * 1_000_000.0) as u64;
let quantity_u64 = (quantity * 1_000_000_000.0) as u64;
orders.push(OrderData {
trader_id: Identity::default(),
price: price_u64,
quantity: quantity_u64,
side: if is_buyer_maker {
Side::Sell
} else {
Side::Buy
},
});
}
Ok(orders)
}
pub async fn get_ethusdc_orders_data(
&self,
date: &str,
limit: usize,
) -> Result<Vec<OrderData>> {
let pair = "ETHUSDC";
self.download_market_data(pair, date).await?;
self.get_order_data(pair, date, limit)
}
pub async fn get_ethusdc_orders_data_summrized(
&self,
date: &str,
limit: usize,
) -> Result<Vec<OrderBookAction>> {
let pair = "ETHUSDC";
self.download_market_data(pair, date).await?;
let all_data = self.get_order_data(pair, date, usize::MAX)?;
if all_data.is_empty() || limit == 0 {
return Ok(Vec::new());
}
let group_size = all_data.len() / limit;
let mut summarized_orders = Vec::new();
for chunk in all_data.chunks_exact(group_size) {
if chunk.is_empty() {
continue;
}
let total_price = chunk.iter().map(|order| order.price).sum::<u64>();
let total_quantity = chunk.iter().map(|order| order.quantity).sum::<u64>();
let avg_price = total_price / chunk.len() as u64;
let avg_quantity = total_quantity / chunk.len() as u64;
let buy_count = chunk
.iter()
.filter(|order| matches!(order.side, Side::Buy))
.count();
let sell_count = chunk.len() - buy_count;
let majority_side = if buy_count > sell_count {
Side::Buy
} else {
Side::Sell
};
let order = OrderBookAction::CreateOrder(OrderData {
trader_id: Identity::default(),
price: avg_price / 1_000_000,
quantity: avg_quantity / 1_000_000,
side: majority_side,
});
summarized_orders.push(order);
}
Ok(summarized_orders)
}
pub fn generate_orderbook_state(
base_asset: AssetId,
quote_asset: AssetId,
actions: &[OrderBookAction],
) -> Book {
let mut order_book = Book::new(BookConfig {
base_asset,
base_decimals: 9,
quote_asset,
quote_decimals: 6,
taker_fee: 0,
maker_fee: 0,
});
for action in actions {
match action {
OrderBookAction::CreateOrder(order) => {
order_book.create_order(order.clone());
}
OrderBookAction::CancelOrder(order_id) => {
order_book.cancel(*order_id);
}
}
}
order_book
}
}