Skip to content

Commit

Permalink
Merge pull request calumrussell#84 from calumrussell/66-add-source-wi…
Browse files Browse the repository at this point in the history
…th-input

66 add source with input
  • Loading branch information
calumrussell authored Jul 3, 2024
2 parents 4b1b42b + 1807227 commit 2377764
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 66 deletions.
180 changes: 180 additions & 0 deletions rotala/src/input/athena.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#![allow(dead_code)]

use std::{borrow::Borrow, collections::HashMap};

use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum Side {
Bid,
Ask,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Level {
pub price: f64,
pub size: f64,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Depth {
pub bids: Vec<Level>,
pub asks: Vec<Level>,
pub date: i64,
}

impl Depth {
pub fn add_level(&mut self, level: Level, side: Side) {
match side {
Side::Bid => {
self.bids.push(level);
self.bids
.sort_by(|x, y| x.price.partial_cmp(&y.price).unwrap());
}
Side::Ask => {
self.asks.push(level);
self.asks
.sort_by(|x, y| x.price.partial_cmp(&y.price).unwrap());
}
}
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BBO {
pub bid: f64,
pub bid_volume: f64,
pub ask: f64,
pub ask_volume: f64,
pub symbol: String,
pub date: i64,
}

pub type DateQuotes = HashMap<String, Depth>;

pub struct Athena {
dates: Vec<i64>,
inner: HashMap<i64, DateQuotes>,
}

impl Athena {
fn get_quotes(&self, date: &i64) -> Option<&DateQuotes> {
self.inner.get(date)
}

fn get_quotes_unchecked(&self, date: &i64) -> &DateQuotes {
self.get_quotes(date).unwrap()
}

pub fn get_date(&self, pos: usize) -> Option<&i64> {
self.dates.get(pos)
}

pub fn has_next(&self, pos: usize) -> bool {
self.dates.len() > pos
}

pub fn get_best_bid(
&self,
date: impl Borrow<i64>,
symbol: impl Into<String>,
) -> Option<&Level> {
if let Some(date_levels) = self.inner.get(date.borrow()) {
if let Some(depth) = date_levels.get(&symbol.into()) {
return depth.bids.last();
}
}
None
}

pub fn get_best_ask(
&self,
date: impl Borrow<i64>,
symbol: impl Into<String>,
) -> Option<&Level> {
if let Some(date_levels) = self.inner.get(date.borrow()) {
if let Some(depth) = date_levels.get(&symbol.into()) {
return depth.asks.first();
}
}
None
}

pub fn add_price_level(
&mut self,
date: i64,
symbol: impl Into<String>,
level: Level,
side: Side,
) {
self.inner.entry(date).or_default();

let symbol_string = symbol.into();

//We will always have a value due to the above block so can unwrap safely
let date_levels = self.inner.get_mut(&date).unwrap();
if let Some(depth) = date_levels.get_mut(&symbol_string) {
depth.add_level(level, side)
} else {
let depth = match side {
Side::Bid => Depth {
bids: vec![level],
asks: vec![],
date,
},
Side::Ask => Depth {
bids: vec![],
asks: vec![level],
date,
},
};

date_levels.insert(symbol_string, depth);
}
}

pub fn new() -> Self {
Self {
dates: Vec::new(),
inner: HashMap::new(),
}
}
}

impl Default for Athena {
fn default() -> Self {
Self::new()
}
}

#[cfg(test)]
mod tests {
use super::{Athena, Level, Side};

#[test]
fn test_that_insertions_are_sorted() {
let mut athena = Athena::new();

let level = Level {
price: 100.0,
size: 100.0,
};

let level1 = Level {
price: 101.0,
size: 100.0,
};

let level2 = Level {
price: 102.0,
size: 100.0,
};

athena.add_price_level(100, "ABC", level2, Side::Ask);
athena.add_price_level(100, "ABC", level1, Side::Bid);
athena.add_price_level(100, "ABC", level, Side::Bid);

assert_eq!(athena.get_best_bid(100, "ABC").unwrap().price, 101.0);
assert_eq!(athena.get_best_ask(100, "ABC").unwrap().price, 102.0);
}
}
1 change: 1 addition & 0 deletions rotala/src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
//!
//! Sources should be called through inputs so that clients do not have to marshall data into internal
//! types.
pub mod athena;
pub mod penelope;
2 changes: 1 addition & 1 deletion rotala/src/input/penelope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use rand::thread_rng;
use rand_distr::{Distribution, Uniform};
use serde::{Deserialize, Serialize};

use crate::source::get_binance_1m_klines;
use crate::source::binance::get_binance_1m_klines;

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PenelopeQuote {
Expand Down
65 changes: 65 additions & 0 deletions rotala/src/source/binance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::io::{Cursor, Write};

pub struct BinanceKlinesQuote {
pub open_date: i64,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
pub close_date: i64,
pub quote_volume: f64,
pub trades_number: i64,
pub taker_buy_volume: f64,
pub taker_buy_asset_volume: f64,
}

// Building this way isn't performant but this is easier to reason about atm
pub fn get_binance_1m_klines() -> Vec<BinanceKlinesQuote> {
let url =
"https://data.binance.vision/data/spot/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2022-08-03.zip";

let mut result = Vec::new();

if let Ok(resp) = reqwest::blocking::get(url) {
if let Ok(contents) = resp.bytes() {
let mut c = Cursor::new(Vec::new());
let _res = c.write(&contents);

if let Ok(mut zip) = zip::ZipArchive::new(c) {
for i in 0..zip.len() {
if let Ok(mut zip_file) = zip.by_index(i) {
let mut rdr = csv::Reader::from_reader(&mut zip_file);
for row in rdr.records().flatten() {
let open_date = (row[0].parse::<i64>().unwrap()) / 1000;
let open = row[1].parse::<f64>().unwrap();
let high = row[2].parse::<f64>().unwrap();
let low = row[3].parse::<f64>().unwrap();
let close = row[4].parse::<f64>().unwrap();
let volume = row[5].parse::<f64>().unwrap();
let close_date = (row[6].parse::<i64>().unwrap()) / 1000;
let quote_volume = row[7].parse::<f64>().unwrap();
let trades_number = row[8].parse::<i64>().unwrap();
let taker_buy_volume = row[9].parse::<f64>().unwrap();
let taker_buy_asset_volume = row[10].parse::<f64>().unwrap();
result.push(BinanceKlinesQuote {
open_date,
open,
high,
low,
close,
volume,
close_date,
quote_volume,
trades_number,
taker_buy_volume,
taker_buy_asset_volume,
});
}
}
}
}
}
}
result
}
48 changes: 48 additions & 0 deletions rotala/src/source/hyperliquid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use std::path::Path;
use std::{collections::HashMap, fs::read_to_string};

use serde::{Deserialize, Serialize};
use serde_json::from_str;

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Level {
pub px: String,
pub sz: String,
pub n: i8,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PointInTime {
pub coin: String,
pub time: u64,
pub levels: Vec<Vec<Level>>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Data {
pub channel: String,
pub data: PointInTime,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct L2Book {
pub time: String,
pub ver_num: u64,
pub raw: Data,
}

pub fn get_hyperliquid_l2(path: &Path) -> HashMap<u64, L2Book> {
let mut result = HashMap::new();
if let Ok(file_contents) = read_to_string(path) {
for line in file_contents.split('\n') {
if line.is_empty() {
continue;
}

let val: L2Book = from_str(line).unwrap();
let time = val.raw.data.time;
result.insert(time, val);
}
}
result
}
67 changes: 2 additions & 65 deletions rotala/src/source/mod.rs
Original file line number Diff line number Diff line change
@@ -1,68 +1,5 @@
//! Sources are external data sources that are used to create Inputs and then Exchanges. Source
//! creation should be hidden from users and embedded within the creation of Inputs. Each Source
//! should have its own internal format that is converted into an Input format within the Input.
use std::io::{Cursor, Write};

pub struct BinanceKlinesQuote {
pub open_date: i64,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
pub close_date: i64,
pub quote_volume: f64,
pub trades_number: i64,
pub taker_buy_volume: f64,
pub taker_buy_asset_volume: f64,
}

// Building this way isn't performant but this is easier to reason about atm
pub fn get_binance_1m_klines() -> Vec<BinanceKlinesQuote> {
let url =
"https://data.binance.vision/data/spot/daily/klines/BTCUSDT/1m/BTCUSDT-1m-2022-08-03.zip";

let mut result = Vec::new();

if let Ok(resp) = reqwest::blocking::get(url) {
if let Ok(contents) = resp.bytes() {
let mut c = Cursor::new(Vec::new());
let _res = c.write(&contents);

if let Ok(mut zip) = zip::ZipArchive::new(c) {
for i in 0..zip.len() {
if let Ok(mut zip_file) = zip.by_index(i) {
let mut rdr = csv::Reader::from_reader(&mut zip_file);
for row in rdr.records().flatten() {
let open_date = (row[0].parse::<i64>().unwrap()) / 1000;
let open = row[1].parse::<f64>().unwrap();
let high = row[2].parse::<f64>().unwrap();
let low = row[3].parse::<f64>().unwrap();
let close = row[4].parse::<f64>().unwrap();
let volume = row[5].parse::<f64>().unwrap();
let close_date = (row[6].parse::<i64>().unwrap()) / 1000;
let quote_volume = row[7].parse::<f64>().unwrap();
let trades_number = row[8].parse::<i64>().unwrap();
let taker_buy_volume = row[9].parse::<f64>().unwrap();
let taker_buy_asset_volume = row[10].parse::<f64>().unwrap();
result.push(BinanceKlinesQuote {
open_date,
open,
high,
low,
close,
volume,
close_date,
quote_volume,
trades_number,
taker_buy_volume,
taker_buy_asset_volume,
});
}
}
}
}
}
}
result
}
pub mod binance;
pub mod hyperliquid;

0 comments on commit 2377764

Please sign in to comment.