diff --git a/Cargo.toml b/Cargo.toml index 62d3050..338df22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" env_logger = "0.11.0" tokio = { version = "1.35.1", features = ["full"] } +derive_more = "0.99.17" [dev-dependencies] criterion = { version="0.5.1", features= ["async_tokio"] } diff --git a/src/bin/jura_server_v1.rs b/src/bin/jura_server_v1.rs index 673b2e4..1754eb3 100644 --- a/src/bin/jura_server_v1.rs +++ b/src/bin/jura_server_v1.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::env; use std::sync::Mutex; @@ -14,19 +15,22 @@ async fn main() -> std::io::Result<()> { let address: String = args[1].clone(); let port: u16 = args[2].parse().unwrap(); - let app_state = web::Data::new(AppState { - exchange: Mutex::new(random_jura_generator(3000).0), - }); + let jura = random_jura_generator(3000); + let mut datasets = HashMap::new(); + datasets.insert("RANDOM".to_string(), jura.0); + + let app_state = Mutex::new(AppState::create(&mut datasets)); + let jura_state = web::Data::new(app_state); HttpServer::new(move || { App::new() - .app_data(app_state.clone()) - .route("/", web::get().to(info)) - .route("/init", web::get().to(init)) - .route("/fetch_quotes", web::get().to(fetch_quotes)) - .route("/tick", web::get().to(tick)) - .route("/insert_order", web::post().to(insert_order)) - .route("/delete_order", web::post().to(delete_order)) + .app_data(jura_state.clone()) + .service(info) + .service(init) + .service(fetch_quotes) + .service(tick) + .service(insert_order) + .service(delete_order) }) .bind((address, port))? .run() diff --git a/src/bin/uist_client_test.rs b/src/bin/uist_client_test.rs index 5c5acec..7370833 100644 --- a/src/bin/uist_client_test.rs +++ b/src/bin/uist_client_test.rs @@ -6,10 +6,14 @@ use rotala::http::uist::uistv1_client::Client; #[tokio::main] async fn main() -> Result<()> { let client = Client::new("http://127.0.0.1:8080".to_string()); - let _ = client.init().await.unwrap(); - if let Ok(check) = client.tick().await { + let resp = client.init("RANDOM".to_string()).await.unwrap(); + let backtest_id = resp.backtest_id; + + if let Ok(check) = client.tick(backtest_id).await { if check.has_next { - let _ = client.insert_order(Order::market_buy("ABC", 100.0)).await; + let _ = client + .insert_order(Order::market_buy("ABC", 100.0), backtest_id) + .await; } } Ok(()) diff --git a/src/bin/uist_server_v1.rs b/src/bin/uist_server_v1.rs index c69aa5d..7801756 100644 --- a/src/bin/uist_server_v1.rs +++ b/src/bin/uist_server_v1.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::env; use std::sync::Mutex; @@ -14,19 +15,22 @@ async fn main() -> std::io::Result<()> { let address: String = args[1].clone(); let port: u16 = args[2].parse().unwrap(); - let app_state = web::Data::new(AppState { - exchange: Mutex::new(random_uist_generator(3000).0), - }); + let uist = random_uist_generator(3000); + let mut datasets = HashMap::new(); + datasets.insert("RANDOM".to_string(), uist.0); + + let app_state = Mutex::new(AppState::create(&mut datasets)); + let uist_state = web::Data::new(app_state); HttpServer::new(move || { App::new() - .app_data(app_state.clone()) - .route("/", web::get().to(info)) - .route("/init", web::get().to(init)) - .route("/fetch_quotes", web::get().to(fetch_quotes)) - .route("/tick", web::get().to(tick)) - .route("/insert_order", web::post().to(insert_order)) - .route("/delete_order", web::post().to(delete_order)) + .app_data(uist_state.clone()) + .service(info) + .service(init) + .service(fetch_quotes) + .service(tick) + .service(insert_order) + .service(delete_order) }) .bind((address, port))? .run() diff --git a/src/exchange/jura_v1.rs b/src/exchange/jura_v1.rs index 7af3825..1dc55a0 100644 --- a/src/exchange/jura_v1.rs +++ b/src/exchange/jura_v1.rs @@ -309,7 +309,7 @@ impl Order { } } -#[derive(Debug)] +#[derive(Clone, Debug)] struct InnerOrder { pub order_id: OrderId, pub order: Order, @@ -345,7 +345,7 @@ impl InfoMessage { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct JuraV1 { dataset: String, clock: Clock, @@ -469,7 +469,7 @@ impl JuraV1 { /// After a trade executes a fill is returned to the user, the data returned is substantially /// different to the Hyperliquid API due to Hyperliquid performing functions like margin. /// The differences are documented in [Fill]. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct OrderBook { inner: VecDeque, last_inserted: u64, diff --git a/src/exchange/uist_v1.rs b/src/exchange/uist_v1.rs index db1e80a..000e708 100644 --- a/src/exchange/uist_v1.rs +++ b/src/exchange/uist_v1.rs @@ -196,7 +196,7 @@ impl InfoMessage { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct UistV1 { dataset: String, clock: Clock, @@ -309,7 +309,7 @@ pub fn random_uist_generator(length: i64) -> (UistV1, Clock) { (UistV1::new(clock.clone(), penelope, "RANDOM"), clock) } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct OrderBook { inner: VecDeque, last_inserted: u64, diff --git a/src/http/jura.rs b/src/http/jura.rs index ce98c59..7221820 100644 --- a/src/http/jura.rs +++ b/src/http/jura.rs @@ -8,23 +8,32 @@ pub mod jurav1_client { use crate::exchange::jura_v1::{InfoMessage, InitMessage, Order, OrderId}; + type BacktestId = u64; + pub struct Client { pub path: String, pub client: reqwest::Client, } impl Client { - pub async fn tick(&self) -> Result { - reqwest::get(self.path.clone() + "/tick") + pub async fn tick(&self, backtest_id: BacktestId) -> Result { + self.client + .get(self.path.clone() + format!("/backtest/{backtest_id}/tick").as_str()) + .send() .await? .json::() .await } - pub async fn delete_order(&self, asset: u64, order_id: OrderId) -> Result<()> { + pub async fn delete_order( + &self, + asset: u64, + order_id: OrderId, + backtest_id: BacktestId, + ) -> Result<()> { let req = DeleteOrderRequest { asset, order_id }; self.client - .post(self.path.clone() + "/delete_order") + .post(self.path.clone() + format!("/backtest/{backtest_id}/delete_order").as_str()) .json(&req) .send() .await? @@ -32,10 +41,10 @@ pub mod jurav1_client { .await } - pub async fn insert_order(&self, order: Order) -> Result<()> { + pub async fn insert_order(&self, order: Order, backtest_id: BacktestId) -> Result<()> { let req = InsertOrderRequest { order }; self.client - .post(self.path.clone() + "/insert_order") + .post(self.path.clone() + format!("/backtest/{backtest_id}/insert_order").as_str()) .json(&req) .send() .await? @@ -43,22 +52,28 @@ pub mod jurav1_client { .await } - pub async fn fetch_quotes(&self) -> Result { - reqwest::get(self.path.clone() + "/fetch_quotes") + pub async fn fetch_quotes(&self, backtest_id: BacktestId) -> Result { + self.client + .get(self.path.clone() + format!("/backtest/{backtest_id}/fetch_quotes").as_str()) + .send() .await? .json::() .await } - pub async fn init(&self) -> Result { - reqwest::get(self.path.clone() + "/init") + pub async fn init(&self, dataset_name: String) -> Result { + self.client + .get(self.path.clone() + format!("/init/{dataset_name}").as_str()) + .send() .await? .json::() .await } - pub async fn info(&self) -> Result { - reqwest::get(self.path.clone() + "/") + pub async fn info(&self, backtest_id: BacktestId) -> Result { + self.client + .get(self.path.clone() + format!("/backtest/{backtest_id}/info").as_str()) + .send() .await? .json::() .await @@ -75,13 +90,79 @@ pub mod jurav1_client { pub mod jurav1_server { use serde::{Deserialize, Serialize}; + use std::collections::HashMap; use std::sync::Mutex; - use crate::exchange::jura_v1::{Fill, JuraQuote, JuraV1, Order, OrderId}; - use actix_web::web; + use crate::exchange::jura_v1::{Fill, InitMessage, JuraQuote, JuraV1, Order, OrderId}; + use actix_web::{ + error, get, post, + web::{self, Path}, + Result, + }; + use derive_more::{Display, Error}; + + type BacktestId = u64; + pub type JuraState = Mutex; + + pub struct BacktestState { + pub id: BacktestId, + pub position: i64, + pub exchange: JuraV1, + } pub struct AppState { - pub exchange: Mutex, + pub exchanges: HashMap, + pub last: BacktestId, + pub datasets: HashMap, + } + + impl AppState { + pub fn create(datasets: &mut HashMap) -> Self { + Self { + exchanges: HashMap::new(), + last: 0, + datasets: std::mem::take(datasets), + } + } + + pub fn new_backtest(&mut self, dataset_name: String) -> Option<(BacktestId, InitMessage)> { + let new_id = self.last + 1; + + if let Some(exchange) = self.datasets.get(&dataset_name) { + // Not efficient but it is easier than breaking the bind between the dataset and + // the exchange. + let copied_exchange = exchange.clone(); + + let init_message = copied_exchange.init(); + + let backtest = BacktestState { + id: new_id, + position: 0, + exchange: copied_exchange, + }; + + self.exchanges.insert(new_id, backtest); + + self.last = new_id; + return Some((new_id, init_message)); + } + None + } + } + + #[derive(Debug, Display, Error)] + pub enum JuraV1Error { + UnknownBacktest, + UnknownDataset, + } + + impl error::ResponseError for JuraV1Error { + fn status_code(&self) -> actix_web::http::StatusCode { + match self { + JuraV1Error::UnknownBacktest => actix_web::http::StatusCode::BAD_REQUEST, + JuraV1Error::UnknownDataset => actix_web::http::StatusCode::BAD_REQUEST, + } + } } #[derive(Debug, Deserialize, Serialize)] @@ -91,15 +172,24 @@ pub mod jurav1_server { pub inserted_orders: Vec, } - pub async fn tick(app: web::Data) -> web::Json { - let mut ex = app.exchange.lock().unwrap(); - - let tick = ex.tick(); - web::Json(TickResponse { - inserted_orders: tick.2, - executed_trades: tick.1, - has_next: tick.0, - }) + #[get("/backtest/{backtest_id}/tick")] + pub async fn tick( + app: web::Data, + path: web::Path<(BacktestId,)>, + ) -> Result, JuraV1Error> { + let mut jura = app.lock().unwrap(); + let (backtest_id,) = path.into_inner(); + + if let Some(state) = jura.exchanges.get_mut(&backtest_id) { + let tick = state.exchange.tick(); + Ok(web::Json(TickResponse { + inserted_orders: tick.2, + executed_trades: tick.1, + has_next: tick.0, + })) + } else { + Err(JuraV1Error::UnknownBacktest) + } } #[derive(Debug, Deserialize, Serialize)] @@ -108,13 +198,22 @@ pub mod jurav1_server { pub order_id: OrderId, } + #[post("/backtest/{backtest_id}/delete_order")] pub async fn delete_order( - app: web::Data, + app: web::Data, + path: web::Path<(BacktestId,)>, delete_order: web::Json, - ) -> web::Json<()> { - let mut ex = app.exchange.lock().unwrap(); - ex.delete_order(delete_order.asset, delete_order.order_id); - web::Json(()) + ) -> Result, JuraV1Error> { + let mut jura = app.lock().unwrap(); + let (backtest_id,) = path.into_inner(); + if let Some(state) = jura.exchanges.get_mut(&backtest_id) { + state + .exchange + .delete_order(delete_order.asset, delete_order.order_id); + Ok(web::Json(())) + } else { + Err(JuraV1Error::UnknownBacktest) + } } #[derive(Debug, Deserialize, Serialize)] @@ -122,19 +221,21 @@ pub mod jurav1_server { pub order: Order, } + #[post("/backtest/{backtest_id}/insert_order")] pub async fn insert_order( - app: web::Data, + app: web::Data, + path: Path<(BacktestId,)>, insert_order: web::Json, - ) -> web::Json<()> { - dbg!(&insert_order); - let mut ex = app.exchange.lock().unwrap(); - ex.insert_order(insert_order.order.clone()); - web::Json(()) - } - - #[derive(Debug, Deserialize, Serialize)] - pub struct FetchTradesRequest { - pub from: OrderId, + ) -> Result, JuraV1Error> { + let mut jura = app.lock().unwrap(); + let (backtest_id,) = path.into_inner(); + + if let Some(state) = jura.exchanges.get_mut(&backtest_id) { + state.exchange.insert_order(insert_order.order.clone()); + Ok(web::Json(())) + } else { + Err(JuraV1Error::UnknownBacktest) + } } #[derive(Debug, Deserialize, Serialize)] @@ -142,26 +243,47 @@ pub mod jurav1_server { pub quotes: Vec, } - pub async fn fetch_quotes(app: web::Data) -> web::Json { - let ex = app.exchange.lock().unwrap(); - web::Json(FetchQuotesResponse { - quotes: ex.fetch_quotes(), - }) + #[get("/backtest/{backtest_id}/fetch_quotes")] + pub async fn fetch_quotes( + app: web::Data, + path: Path<(BacktestId,)>, + ) -> Result, JuraV1Error> { + let mut jura = app.lock().unwrap(); + let (backtest_id,) = path.into_inner(); + + if let Some(state) = jura.exchanges.get_mut(&backtest_id) { + Ok(web::Json(FetchQuotesResponse { + quotes: state.exchange.fetch_quotes(), + })) + } else { + Err(JuraV1Error::UnknownBacktest) + } } #[derive(Debug, Deserialize, Serialize)] pub struct InitResponse { + pub backtest_id: BacktestId, pub start: i64, pub frequency: u8, } - pub async fn init(app: web::Data) -> web::Json { - let ex = app.exchange.lock().unwrap(); - let init = ex.init(); - web::Json(InitResponse { - start: init.start, - frequency: init.frequency, - }) + #[get("/init/{dataset_name}")] + pub async fn init( + app: web::Data, + path: Path<(String,)>, + ) -> Result, JuraV1Error> { + let mut jura = app.lock().unwrap(); + let (dataset_name,) = path.into_inner(); + + if let Some(backtest) = jura.new_backtest(dataset_name) { + Ok(web::Json(InitResponse { + backtest_id: backtest.0, + start: backtest.1.start, + frequency: backtest.1.frequency, + })) + } else { + Err(JuraV1Error::UnknownDataset) + } } #[derive(Debug, Deserialize, Serialize)] @@ -170,13 +292,23 @@ pub mod jurav1_server { pub dataset: String, } - pub async fn info(app: web::Data) -> web::Json { - let ex = app.exchange.lock().unwrap(); - let info = ex.info(); - web::Json(InfoResponse { - version: info.version, - dataset: info.dataset, - }) + #[get("/backtest/{backtest_id}/info")] + pub async fn info( + app: web::Data, + path: Path<(BacktestId,)>, + ) -> Result, JuraV1Error> { + let mut jura = app.lock().unwrap(); + let (backtest_id,) = path.into_inner(); + + if let Some(state) = jura.exchanges.get_mut(&backtest_id) { + let info = state.exchange.info(); + Ok(web::Json(InfoResponse { + version: info.version, + dataset: info.dataset, + })) + } else { + Err(JuraV1Error::UnknownBacktest) + } } } @@ -187,45 +319,60 @@ mod tests { use crate::exchange::jura_v1::{random_jura_generator, Order}; use super::jurav1_server::*; - use std::sync::Mutex; + use std::{collections::HashMap, sync::Mutex}; #[actix_web::test] async fn test_single_trade_loop() { - let app_state = web::Data::new(AppState { - exchange: Mutex::new(random_jura_generator(3000).0), - }); + let jura = random_jura_generator(3000); + let dataset_name = "random"; + + let mut datasets = HashMap::new(); + datasets.insert(dataset_name.to_string(), jura.0); + + let app_state = Mutex::new(AppState::create(&mut datasets)); + let jura_state = web::Data::new(app_state); let app = test::init_service( App::new() - .app_data(app_state.clone()) - .route("/", web::get().to(info)) - .route("/init", web::get().to(init)) - .route("/fetch_quotes", web::get().to(fetch_quotes)) - .route("/tick", web::get().to(tick)) - .route("/insert_order", web::post().to(insert_order)) - .route("/delete_order", web::post().to(delete_order)), + .app_data(jura_state) + .service(info) + .service(init) + .service(fetch_quotes) + .service(tick) + .service(insert_order) + .service(delete_order), ) .await; - let req = test::TestRequest::get().uri("/init").to_request(); + let req = test::TestRequest::get() + .uri(format!("/init/{dataset_name}").as_str()) + .to_request(); let resp: InitResponse = test::call_and_read_body_json(&app, req).await; assert!(resp.frequency == 0); - let req1 = test::TestRequest::get().uri("/fetch_quotes").to_request(); + let backtest_id = resp.backtest_id; + + let req1 = test::TestRequest::get() + .uri(format!("/backtest/{backtest_id}/fetch_quotes").as_str()) + .to_request(); let _resp1: FetchQuotesResponse = test::call_and_read_body_json(&app, req1).await; - let req2 = test::TestRequest::get().uri("/tick").to_request(); + let req2 = test::TestRequest::get() + .uri(format!("/backtest/{backtest_id}/tick").as_str()) + .to_request(); let _resp2: TickResponse = test::call_and_read_body_json(&app, req2).await; let req3 = test::TestRequest::post() .set_json(InsertOrderRequest { order: Order::market_buy(0, "100.0", "97.00"), }) - .uri("/insert_order") + .uri(format!("/backtest/{backtest_id}/insert_order").as_str()) .to_request(); test::call_and_read_body(&app, req3).await; - let req4 = test::TestRequest::get().uri("/tick").to_request(); + let req4 = test::TestRequest::get() + .uri(format!("/backtest/{backtest_id}/tick").as_str()) + .to_request(); let resp4: TickResponse = test::call_and_read_body_json(&app, req4).await; assert!(resp4.executed_trades.len() == 1); diff --git a/src/http/uist.rs b/src/http/uist.rs index 535a342..42827c2 100644 --- a/src/http/uist.rs +++ b/src/http/uist.rs @@ -3,10 +3,13 @@ pub mod uistv1_client { use reqwest::Result; use super::uistv1_server::{ - DeleteOrderRequest, FetchQuotesResponse, InsertOrderRequest, TickResponse, + DeleteOrderRequest, FetchQuotesResponse, InfoResponse, InitResponse, InsertOrderRequest, + TickResponse, }; - use crate::exchange::uist_v1::{InfoMessage, InitMessage, Order, OrderId}; + use crate::exchange::uist_v1::{Order, OrderId}; + + type BacktestId = u64; pub struct Client { pub path: String, @@ -14,17 +17,19 @@ pub mod uistv1_client { } impl Client { - pub async fn tick(&self) -> Result { - reqwest::get(self.path.clone() + "/tick") + pub async fn tick(&self, backtest_id: BacktestId) -> Result { + self.client + .get(self.path.clone() + format!("/backtest/{backtest_id}/tick").as_str()) + .send() .await? .json::() .await } - pub async fn delete_order(&self, order_id: OrderId) -> Result<()> { + pub async fn delete_order(&self, order_id: OrderId, backtest_id: BacktestId) -> Result<()> { let req = DeleteOrderRequest { order_id }; self.client - .post(self.path.clone() + "/delete_order") + .post(self.path.clone() + format!("/backtest/{backtest_id}/delete_order").as_str()) .json(&req) .send() .await? @@ -32,10 +37,10 @@ pub mod uistv1_client { .await } - pub async fn insert_order(&self, order: Order) -> Result<()> { + pub async fn insert_order(&self, order: Order, backtest_id: BacktestId) -> Result<()> { let req = InsertOrderRequest { order }; self.client - .post(self.path.clone() + "/insert_order") + .post(self.path.clone() + format!("/backtest/{backtest_id}/insert_order").as_str()) .json(&req) .send() .await? @@ -43,24 +48,30 @@ pub mod uistv1_client { .await } - pub async fn fetch_quotes(&self) -> Result { - reqwest::get(self.path.clone() + "/fetch_quotes") + pub async fn fetch_quotes(&self, backtest_id: BacktestId) -> Result { + self.client + .get(self.path.clone() + format!("/backtest/{backtest_id}/fetch_quotes").as_str()) + .send() .await? .json::() .await } - pub async fn init(&self) -> Result { - reqwest::get(self.path.clone() + "/init") + pub async fn init(&self, dataset_name: String) -> Result { + self.client + .get(self.path.clone() + format!("/init/{dataset_name}").as_str()) + .send() .await? - .json::() + .json::() .await } - pub async fn info(&self) -> Result { - reqwest::get(self.path.clone() + "/") + pub async fn info(&self, backtest_id: BacktestId) -> Result { + self.client + .get(self.path.clone() + format!("/backtest/{backtest_id}/info").as_str()) + .send() .await? - .json::() + .json::() .await } @@ -75,13 +86,74 @@ pub mod uistv1_client { pub mod uistv1_server { use serde::{Deserialize, Serialize}; - use std::sync::Mutex; + use std::{collections::HashMap, sync::Mutex}; - use crate::exchange::uist_v1::{Order, OrderId, Trade, UistQuote, UistV1}; - use actix_web::web; + use crate::exchange::uist_v1::{InitMessage, Order, OrderId, Trade, UistQuote, UistV1}; + use actix_web::{get, post, web, ResponseError}; + use derive_more::{Display, Error}; + + type BacktestId = u64; + pub type UistState = Mutex; + + pub struct BacktestState { + pub id: BacktestId, + pub position: i64, + pub exchange: UistV1, + } pub struct AppState { - pub exchange: Mutex, + pub exchanges: HashMap, + pub last: BacktestId, + pub datasets: HashMap, + } + + impl AppState { + pub fn create(datasets: &mut HashMap) -> Self { + Self { + exchanges: HashMap::new(), + last: 0, + datasets: std::mem::take(datasets), + } + } + + pub fn new_backtest(&mut self, dataset_name: String) -> Option<(BacktestId, InitMessage)> { + let new_id = self.last + 1; + + if let Some(exchange) = self.datasets.get(&dataset_name) { + // Not efficient but it is easier than breaking the bind between the dataset and + // the exchange. + let copied_exchange = exchange.clone(); + + let init_message = copied_exchange.init(); + + let backtest = BacktestState { + id: new_id, + position: 0, + exchange: copied_exchange, + }; + + self.exchanges.insert(new_id, backtest); + + self.last = new_id; + return Some((new_id, init_message)); + } + None + } + } + + #[derive(Debug, Display, Error)] + pub enum UistV1Error { + UnknownBacktest, + UnknownDataset, + } + + impl ResponseError for UistV1Error { + fn status_code(&self) -> actix_web::http::StatusCode { + match self { + UistV1Error::UnknownBacktest => actix_web::http::StatusCode::BAD_REQUEST, + UistV1Error::UnknownDataset => actix_web::http::StatusCode::BAD_REQUEST, + } + } } #[derive(Debug, Deserialize, Serialize)] @@ -91,15 +163,24 @@ pub mod uistv1_server { pub inserted_orders: Vec, } - pub async fn tick(app: web::Data) -> web::Json { - let mut ex = app.exchange.lock().unwrap(); - - let tick = ex.tick(); - web::Json(TickResponse { - inserted_orders: tick.2, - executed_trades: tick.1, - has_next: tick.0, - }) + #[get("/backtest/{backtest_id}/tick")] + pub async fn tick( + app: web::Data, + path: web::Path<(BacktestId,)>, + ) -> Result, UistV1Error> { + let mut uist = app.lock().unwrap(); + let (backtest_id,) = path.into_inner(); + + if let Some(state) = uist.exchanges.get_mut(&backtest_id) { + let tick = state.exchange.tick(); + Ok(web::Json(TickResponse { + inserted_orders: tick.2, + executed_trades: tick.1, + has_next: tick.0, + })) + } else { + Err(UistV1Error::UnknownBacktest) + } } #[derive(Debug, Deserialize, Serialize)] @@ -107,13 +188,21 @@ pub mod uistv1_server { pub order_id: OrderId, } + #[post("/backtest/{backtest_id}/delete_order")] pub async fn delete_order( - app: web::Data, + app: web::Data, + path: web::Path<(BacktestId,)>, delete_order: web::Json, - ) -> web::Json<()> { - let mut ex = app.exchange.lock().unwrap(); - ex.delete_order(delete_order.order_id); - web::Json(()) + ) -> Result, UistV1Error> { + let mut uist = app.lock().unwrap(); + let (backtest_id,) = path.into_inner(); + + if let Some(state) = uist.exchanges.get_mut(&backtest_id) { + state.exchange.delete_order(delete_order.order_id); + Ok(web::Json(())) + } else { + Err(UistV1Error::UnknownBacktest) + } } #[derive(Debug, Deserialize, Serialize)] @@ -121,19 +210,20 @@ pub mod uistv1_server { pub order: Order, } + #[post("/backtest/{backtest_id}/insert_order")] pub async fn insert_order( - app: web::Data, + app: web::Data, + path: web::Path<(BacktestId,)>, insert_order: web::Json, - ) -> web::Json<()> { - dbg!(&insert_order); - let mut ex = app.exchange.lock().unwrap(); - ex.insert_order(insert_order.order.clone()); - web::Json(()) - } - - #[derive(Debug, Deserialize, Serialize)] - pub struct FetchTradesRequest { - pub from: OrderId, + ) -> Result, UistV1Error> { + let mut uist = app.lock().unwrap(); + let (backtest_id,) = path.into_inner(); + if let Some(state) = uist.exchanges.get_mut(&backtest_id) { + state.exchange.insert_order(insert_order.order.clone()); + Ok(web::Json(())) + } else { + Err(UistV1Error::UnknownBacktest) + } } #[derive(Debug, Deserialize, Serialize)] @@ -141,26 +231,47 @@ pub mod uistv1_server { pub quotes: Vec, } - pub async fn fetch_quotes(app: web::Data) -> web::Json { - let ex = app.exchange.lock().unwrap(); - web::Json(FetchQuotesResponse { - quotes: ex.fetch_quotes(), - }) + #[get("/backtest/{backtest_id}/fetch_quotes")] + pub async fn fetch_quotes( + app: web::Data, + path: web::Path<(BacktestId,)>, + ) -> Result, UistV1Error> { + let mut uist = app.lock().unwrap(); + let (backtest_id,) = path.into_inner(); + + if let Some(state) = uist.exchanges.get_mut(&backtest_id) { + Ok(web::Json(FetchQuotesResponse { + quotes: state.exchange.fetch_quotes(), + })) + } else { + Err(UistV1Error::UnknownBacktest) + } } #[derive(Debug, Deserialize, Serialize)] pub struct InitResponse { + pub backtest_id: BacktestId, pub start: i64, pub frequency: u8, } - pub async fn init(app: web::Data) -> web::Json { - let ex = app.exchange.lock().unwrap(); - let init = ex.init(); - web::Json(InitResponse { - start: init.start, - frequency: init.frequency, - }) + #[get("/init/{dataset_name}")] + pub async fn init( + app: web::Data, + path: web::Path<(String,)>, + ) -> Result, UistV1Error> { + let mut uist = app.lock().unwrap(); + let (dataset_name,) = path.into_inner(); + + if let Some(backtest) = uist.new_backtest(dataset_name) { + Ok(web::Json(InitResponse { + backtest_id: backtest.0, + start: backtest.1.start, + frequency: backtest.1.frequency, + })) + } else { + Err(UistV1Error::UnknownDataset) + } } #[derive(Debug, Deserialize, Serialize)] @@ -169,13 +280,22 @@ pub mod uistv1_server { pub dataset: String, } - pub async fn info(app: web::Data) -> web::Json { - let ex = app.exchange.lock().unwrap(); - let info = ex.info(); - web::Json(InfoResponse { - version: info.version, - dataset: info.dataset, - }) + #[get("/backtest/{backtest_id}/info")] + pub async fn info( + app: web::Data, + path: web::Path<(BacktestId,)>, + ) -> Result, UistV1Error> { + let mut uist = app.lock().unwrap(); + let (backtest_id,) = path.into_inner(); + if let Some(state) = uist.exchanges.get_mut(&backtest_id) { + let info = state.exchange.info(); + Ok(web::Json(InfoResponse { + version: info.version, + dataset: info.dataset, + })) + } else { + Err(UistV1Error::UnknownBacktest) + } } } @@ -186,45 +306,60 @@ mod tests { use crate::exchange::uist_v1::{random_uist_generator, Order}; use super::uistv1_server::*; - use std::sync::Mutex; + use std::{collections::HashMap, sync::Mutex}; #[actix_web::test] async fn test_single_trade_loop() { - let app_state = web::Data::new(AppState { - exchange: Mutex::new(random_uist_generator(3000).0), - }); + let uist = random_uist_generator(3000); + let dataset_name = "random"; + + let mut datasets = HashMap::new(); + datasets.insert(dataset_name.to_string(), uist.0); + + let app_state = Mutex::new(AppState::create(&mut datasets)); + let uist_state = web::Data::new(app_state); let app = test::init_service( App::new() - .app_data(app_state.clone()) - .route("/", web::get().to(info)) - .route("/init", web::get().to(init)) - .route("/fetch_quotes", web::get().to(fetch_quotes)) - .route("/tick", web::get().to(tick)) - .route("/insert_order", web::post().to(insert_order)) - .route("/delete_order", web::post().to(delete_order)), + .app_data(uist_state) + .service(info) + .service(init) + .service(fetch_quotes) + .service(tick) + .service(insert_order) + .service(delete_order), ) .await; - let req = test::TestRequest::get().uri("/init").to_request(); + let req = test::TestRequest::get() + .uri(format!("/init/{dataset_name}").as_str()) + .to_request(); let resp: InitResponse = test::call_and_read_body_json(&app, req).await; assert!(resp.frequency == 0); - let req1 = test::TestRequest::get().uri("/fetch_quotes").to_request(); + let backtest_id = resp.backtest_id; + + let req1 = test::TestRequest::get() + .uri(format!("/backtest/{backtest_id}/fetch_quotes").as_str()) + .to_request(); let _resp1: FetchQuotesResponse = test::call_and_read_body_json(&app, req1).await; - let req2 = test::TestRequest::get().uri("/tick").to_request(); + let req2 = test::TestRequest::get() + .uri(format!("/backtest/{backtest_id}/tick").as_str()) + .to_request(); let _resp2: TickResponse = test::call_and_read_body_json(&app, req2).await; let req3 = test::TestRequest::post() .set_json(InsertOrderRequest { order: Order::market_buy("ABC", 100.0), }) - .uri("/insert_order") + .uri(format!("/backtest/{backtest_id}/insert_order").as_str()) .to_request(); test::call_and_read_body(&app, req3).await; - let req4 = test::TestRequest::get().uri("/tick").to_request(); + let req4 = test::TestRequest::get() + .uri(format!("/backtest/{backtest_id}/tick").as_str()) + .to_request(); let resp4: TickResponse = test::call_and_read_body_json(&app, req4).await; assert!(resp4.executed_trades.len() == 1); diff --git a/src/input/penelope.rs b/src/input/penelope.rs index eb81380..3a00626 100644 --- a/src/input/penelope.rs +++ b/src/input/penelope.rs @@ -19,7 +19,7 @@ pub trait PenelopeQuote { fn create(bid: f64, ask: f64, date: i64, symbol: String) -> Self; } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Penelope { inner: HashMap>, }