diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cbf7bd404..3b13535b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -272,7 +272,7 @@ jobs: run: RUST_BACKTRACE=1 ${{ matrix.tests }} --nocapture --ignored - name: Print maker logs on e2e tests error if: failure() - run: docker logs maker + run: cat data/maker/regtest.log - name: Print coordinator logs on e2e tests error if: failure() run: cat data/coordinator/regtest.log diff --git a/crates/commons/src/lib.rs b/crates/commons/src/lib.rs index 39d28eb40..220582c11 100644 --- a/crates/commons/src/lib.rs +++ b/crates/commons/src/lib.rs @@ -1,5 +1,4 @@ use bitcoin::secp256k1::PublicKey; -use rust_decimal::prelude::ToPrimitive; use serde::Deserialize; use serde::Serialize; diff --git a/crates/commons/src/price.rs b/crates/commons/src/price.rs index cb123d290..af42a0afb 100644 --- a/crates/commons/src/price.rs +++ b/crates/commons/src/price.rs @@ -1,10 +1,10 @@ use crate::order::Order; use crate::order::OrderState; -use crate::ToPrimitive; use rust_decimal::Decimal; use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; +use time::OffsetDateTime; use trade::ContractSymbol; use trade::Direction; @@ -33,38 +33,52 @@ pub fn best_current_price(current_orders: &[Order]) -> Prices { prices } -/// Best price (highest) of all long (buy) orders in the orderbook -fn best_bid_price(current_orders: &[Order], symbol: ContractSymbol) -> Option { - best_price_for(current_orders, Direction::Long, symbol) -} - -/// Best price (lowest) of all short (sell) orders in the orderbook -fn best_ask_price(current_orders: &[Order], symbol: ContractSymbol) -> Option { - best_price_for(current_orders, Direction::Short, symbol) +/// If you place a market order to go short/sell, the best/highest `Bid` price +/// +/// Differently said, remember `buy high`, `sell low`! +/// Ask = high +/// Bid = low +/// +/// The best `Ask` is the lowest of all `Asks` +/// The best `Bid` is the highest of all `Bids` +/// +/// If you SELL, you ask and you get the best price someone is willing to buy at i.e. the highest +/// bid price. +fn best_bid_price(orders: &[Order], symbol: ContractSymbol) -> Option { + orders + .iter() + .filter(|o| { + o.order_state == OrderState::Open + && o.direction == Direction::Long + && o.contract_symbol == symbol + && o.expiry > OffsetDateTime::now_utc() + }) + .map(|o| o.price) + .max() } -fn best_price_for( - current_orders: &[Order], - direction: Direction, - symbol: ContractSymbol, -) -> Option { - assert_eq!( - symbol, - ContractSymbol::BtcUsd, - "only btcusd supported for now" - ); - let use_max = direction == Direction::Long; - current_orders +/// If you place a market order to go long/buy, you get the best/lowest `Ask` price +/// +/// Differently said, remember `buy high`, `sell low`! +/// Ask = high +/// Bid = low +/// +/// The best `Ask` is the lowest of all `Asks` +/// The best `Bid` is the highest of all `Bids` +/// +/// If you BUY, you bid and you get the best price someone is willing to sell at i.e. the lowest ask +/// price. +fn best_ask_price(orders: &[Order], symbol: ContractSymbol) -> Option { + orders .iter() - .filter(|order| order.order_state == OrderState::Open && order.direction == direction) - .map(|order| order.price.to_f64().expect("to represent decimal as f64")) - // get the best price - .fold(None, |acc, x| match acc { - Some(y) => Some(if use_max { x.max(y) } else { x.min(y) }), - None => Some(x), - })? - .try_into() - .ok() + .filter(|o| { + o.order_state == OrderState::Open + && o.direction == Direction::Short + && o.contract_symbol == symbol + && o.expiry > OffsetDateTime::now_utc() + }) + .map(|o| o.price) + .min() } #[cfg(test)] @@ -79,6 +93,7 @@ mod test { use rust_decimal::Decimal; use rust_decimal_macros::dec; use std::str::FromStr; + use time::Duration; use time::OffsetDateTime; use trade::ContractSymbol; use trade::Direction; @@ -101,7 +116,7 @@ mod test { quantity: 100.into(), order_type: OrderType::Market, timestamp: OffsetDateTime::now_utc(), - expiry: OffsetDateTime::now_utc(), + expiry: OffsetDateTime::now_utc() + Duration::minutes(1), order_state, order_reason: OrderReason::Manual, stable: false, diff --git a/crates/dev-maker/src/main.rs b/crates/dev-maker/src/main.rs index 02f4af135..980d2cf40 100644 --- a/crates/dev-maker/src/main.rs +++ b/crates/dev-maker/src/main.rs @@ -22,11 +22,13 @@ mod historic_rates; mod logger; mod orderbook_client; +const ORDER_EXPIRY: u64 = 30; + #[tokio::main] async fn main() -> Result<()> { init_tracing(LevelFilter::DEBUG)?; - let client = OrderbookClient::new(Url::from_str("http://localhost:8000/api/orderbook/orders")?); + let client = OrderbookClient::new(Url::from_str("http://localhost:8000")?); let secret_key = SecretKey::new(&mut rand::thread_rng()); let public_key = secret_key.public_key(SECP256K1); @@ -35,26 +37,49 @@ async fn main() -> Result<()> { let mut historic_rates = historic_rates::read(); historic_rates.sort_by(|a, b| a.timestamp.cmp(&b.timestamp)); + let mut past_ids = vec![]; loop { for historic_rate in &historic_rates { - post_order( - client.clone(), - secret_key, - public_key, - Direction::Short, - historic_rate.open + Decimal::from(1), - ) - .await; - post_order( - client.clone(), - secret_key, - public_key, - Direction::Long, - historic_rate.open + Decimal::from(1), - ) - .await; + let mut tmp_ids = vec![]; + for _ in 0..5 { + tmp_ids.push( + post_order( + client.clone(), + secret_key, + public_key, + Direction::Short, + historic_rate.open + Decimal::from(1), + ORDER_EXPIRY, + ) + .await, + ); + tmp_ids.push( + post_order( + client.clone(), + secret_key, + public_key, + Direction::Long, + historic_rate.open - Decimal::from(1), + ORDER_EXPIRY, + ) + .await, + ); + } + + for old_id in &past_ids { + if let Err(err) = client.delete_order(old_id).await { + tracing::error!( + "Could not delete old order with id {old_id} because of {err:?}" + ); + } + } + + past_ids.clear(); + + past_ids.extend(tmp_ids); - sleep(Duration::from_secs(60)).await; + // we sleep a bit shorter than the last order expires to ensure always having an order + sleep(Duration::from_secs(ORDER_EXPIRY - 1)).await; } } } @@ -69,11 +94,13 @@ async fn post_order( public_key: PublicKey, direction: Direction, price: Decimal, -) { + order_expiry_seconds: u64, +) -> Uuid { + let uuid = Uuid::new_v4(); if let Err(err) = client .post_new_order( NewOrder { - id: Uuid::new_v4(), + id: uuid, contract_symbol: ContractSymbol::BtcUsd, price, quantity: Decimal::from(5000), @@ -81,7 +108,8 @@ async fn post_order( direction, leverage: Decimal::from(2), order_type: OrderType::Limit, - expiry: OffsetDateTime::now_utc() + time::Duration::minutes(1), + expiry: OffsetDateTime::now_utc() + + time::Duration::seconds(order_expiry_seconds as i64), stable: false, }, None, @@ -91,4 +119,5 @@ async fn post_order( { tracing::error!("Failed posting new order {err:?}"); } + uuid } diff --git a/crates/dev-maker/src/orderbook_client.rs b/crates/dev-maker/src/orderbook_client.rs index be7f92c63..17a75dc4b 100644 --- a/crates/dev-maker/src/orderbook_client.rs +++ b/crates/dev-maker/src/orderbook_client.rs @@ -5,6 +5,7 @@ use commons::NewOrderRequest; use reqwest::Client; use reqwest::Url; use secp256k1::SecretKey; +use uuid::Uuid; #[derive(Clone)] pub struct OrderbookClient { @@ -27,6 +28,8 @@ impl OrderbookClient { channel_opening_params: Option, secret_key: SecretKey, ) -> Result<()> { + let url = self.url.join("/api/orderbook/orders")?; + tracing::info!( id = order.id.to_string(), direction = order.direction.to_string(), @@ -43,7 +46,7 @@ impl OrderbookClient { let response = self .client - .post(self.url.clone()) + .post(url) .json(&new_order_request) .send() .await?; @@ -52,4 +55,23 @@ impl OrderbookClient { Ok(()) } + + pub async fn delete_order(&self, order_id: &Uuid) -> Result<()> { + tracing::debug!( + order_id = order_id.to_string(), + "Deleting order from orderbook" + ); + + let url = self.url.join( + format!("/api/orderbook/orders/{}", order_id) + .to_string() + .as_str(), + )?; + + let response = self.client.delete(url).send().await?; + + response.error_for_status()?; + + Ok(()) + } } diff --git a/justfile b/justfile index 3e253dd40..be19e9743 100644 --- a/justfile +++ b/justfile @@ -292,17 +292,11 @@ coordinator args="": cargo run --bin coordinator -- {{args}} -run_maker_args := if os() == "linux" { - "--network=\"host\" --pull always --name maker ghcr.io/get10101/aristides/aristides:main regtest --orderbook http://localhost:8000" -} else if os() == "macos" { - "--pull always --name maker ghcr.io/get10101/aristides/aristides:main regtest --orderbook http://host.docker.internal:8000" -} else { - "echo 'Only linux and macos are supported'; - exit" -} - maker args="": - cargo run --bin dev-maker + #!/usr/bin/env bash + set -euxo pipefail + + cargo run --bin dev-maker -- {{args}} flutter-test: cd mobile && fvm flutter pub run build_runner build --delete-conflicting-outputs && fvm flutter test @@ -350,20 +344,18 @@ run-coordinator-detached: just wait-for-coordinator-to-be-ready echo "Coordinator successfully started. You can inspect the logs at {{coordinator_log_file}}" -run_maker_detached_args := if os() == "linux" { - "--network=\"host\" --pull always -d --name maker ghcr.io/get10101/aristides/aristides:main regtest --orderbook http://localhost:8000" -} else if os() == "macos" { - "--pull always -d --name maker ghcr.io/get10101/aristides/aristides:main regtest --orderbook http://host.docker.internal:8000" -} else { - "echo 'Only linux and macos are supported'; - exit" -} - # Starts maker process in the background, piping logs to a file (used in other recipes) run-maker-detached: - # we always delete the old container first as otherwise we might get an error if the container still exists - docker rm -f maker || true - docker run {{run_maker_detached_args}} + #!/usr/bin/env bash + set -euxo pipefail + + echo "Building maker first" + cargo build --bin dev-maker + echo "Starting (and building) maker" + + just maker &> {{maker_log_file}} & + just wait-for-maker-to-be-ready + echo "Maker successfully started. You can inspect the logs at {{maker_log_file}}" # Attach to the current coordinator logs coordinator-logs: @@ -449,6 +441,30 @@ wait-for-coordinator-to-be-ready: echo "Max attempts reached. Coordinator is still not ready." exit 1 +[private] +wait-for-maker-to-be-ready: + #!/usr/bin/env bash + set +e + + MAX_RETRIES=30 + RETRY_DELAY=2 + + for ((i=1; i<=$MAX_RETRIES; i++)); do + response=$(curl -s http://localhost:8000/api/orderbook/orders) + item_count=$(echo "$response" | jq '. | length') + + if [[ $item_count -ge 2 ]]; then + echo "Request successful, found $item_count items." + exit 0 + else + echo "Retry $i: Found $item_count items. Retrying in $RETRY_DELAY seconds..." + sleep $RETRY_DELAY + fi + done + + echo "Maximum retries exceeded. Starting maker failed." + exit 1 + build-ipa args="": #!/usr/bin/env bash BUILD_NUMBER=$(git rev-list HEAD --count)