Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- main
- dev-*
pull_request:
branches:
- main
Expand All @@ -21,8 +22,7 @@ jobs:
uses: actions-rs/toolchain@v1
with:
toolchain: 1.86.0
# components: clippy, rustfmt
components: rustfmt
components: clippy, rustfmt

- name: Cache Cargo registry
uses: actions/cache@v4
Expand All @@ -48,5 +48,5 @@ jobs:
- name: Run cargo test
run: cargo test --workspace --all-targets --no-fail-fast

# - name: Run cargo clippy
# run: cargo clippy --workspace --all-targets -- -D warnings
- name: Run cargo clippy
run: cargo clippy --workspace --all-targets -- -D warnings
20 changes: 11 additions & 9 deletions apex-core/benches/common.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
use apex_core::prelude::*;
use std::cell::UnsafeCell;

/// Quickly generate a simple limit order for testing
pub fn make_limit_order(id: u64, side: Side, price: u64, qty: u64, ts: u64) -> Order {
let mut value = Order::default();
value.id = id;
value.user_id = 1;
value.side = side;
value.price = Price::from(price);
*value.quantity.get_mut() = Quantity::from(qty);
value.created_at = ts;
value.updated_at = ts;
value
Order {
id,
user_id: 1,
side,
price: Price::from(price),
quantity: UnsafeCell::new(Quantity::from(qty)),
created_at: ts,
updated_at: ts,
..Order::default()
}
}

/// Quickly generate a market order for testing
Expand Down
4 changes: 2 additions & 2 deletions apex-core/src/engine.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
pub mod book;
pub mod engine;
pub mod error;
pub mod matching;
pub mod syncer;
pub mod types;

pub mod prelude {
pub use super::book::*;
pub use super::engine::*;
pub use super::error::*;
pub use super::matching::*;
pub use super::syncer::*;
pub use super::types::*;
}
29 changes: 14 additions & 15 deletions apex-core/src/engine/book.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub trait OrderBook {
/// Get the book
fn get_book(&self, side: Side) -> &SkipList<BookKey, Order>;
/// Sync orders that's matched and trades
fn sync_matched(&self, updated: &Vec<Order>, trades: &Vec<Trade>);
fn sync_matched(&self, updated: &[Order], trades: &[Trade]);
}

/// WalkingResult is used for match engine walking results
Expand Down Expand Up @@ -239,7 +239,7 @@ impl OrderBook for DefaultOrderBook {
order_entry.remove();
order_index.remove(&order_id);
let id = self.id.fetch_add(1, Ordering::Acquire);
self.syncer.cancel_order(id, &book_order);
self.syncer.cancel_order(id, book_order);

Ok(())
}
Expand All @@ -251,10 +251,7 @@ impl OrderBook for DefaultOrderBook {
Side::Buy => self.buy_orders.front(guard),
Side::Sell => self.sell_orders.front(guard),
};
match entry {
Some(entry) => Some(entry.key().price),
None => None,
}
entry.map(|entry| entry.key().price)
}

fn get_book(&self, side: Side) -> &SkipList<BookKey, Order> {
Expand All @@ -265,7 +262,7 @@ impl OrderBook for DefaultOrderBook {
}

/// Sync orders that are matched and trades
fn sync_matched(&self, updated: &Vec<Order>, trades: &Vec<Trade>) {
fn sync_matched(&self, updated: &[Order], trades: &[Trade]) {
let id = self.id.fetch_add(1, Ordering::Acquire);
self.syncer.matched(id, updated, trades);
}
Expand Down Expand Up @@ -363,14 +360,16 @@ impl MatchingEngineWalker for DefaultOrderBook {
continue;
}

let taker = if buy_maker_only && !sell_maker_only {
sell_order
} else if sell_maker_only && !buy_maker_only {
buy_order
} else if buy_key.priority < sell_key.priority {
buy_order
} else {
sell_order
let taker = match (buy_maker_only, sell_maker_only) {
(true, false) => sell_order,
(false, true) => buy_order,
_ => {
if buy_key.priority < sell_key.priority {
buy_order
} else {
sell_order
}
}
};
let taker_is_buy = taker.side == Side::Buy;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ impl DefaultMatchingEngine {
self.order_book
.walking_by_order_id_list(order_id_list.as_slice(), &mut |o| {
o.exit_matched();
return WalkingResult::next();
WalkingResult::next()
});
None
}
Expand Down
4 changes: 2 additions & 2 deletions apex-core/src/engine/syncer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub trait OrderBookSyncer: Send + Sync {
/// This function is called when the order book cancels an order
fn cancel_order(&self, id: u64, order: &Order);
/// This function is called when the order engine matches an order
fn matched(&self, id: u64, updated: &Vec<Order>, trades: &Vec<Trade>);
fn matched(&self, id: u64, updated: &[Order], trades: &[Trade]);
}

/// EmptyOrderBookSyncer is a no-op implementation of OrderBookSyncer
Expand All @@ -22,5 +22,5 @@ impl OrderBookSyncer for EmptyOrderBookSyncer {

fn cancel_order(&self, _id: u64, _order: &Order) {}

fn matched(&self, _id: u64, _updated: &Vec<Order>, _trades: &Vec<Trade>) {}
fn matched(&self, _id: u64, _updated: &[Order], _trades: &[Trade]) {}
}
84 changes: 39 additions & 45 deletions apex-core/src/engine/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub enum OrderStatus {
/// - `Active` → `Finished` (cancellation thread removes order)
/// - `Matched` → `Active` (matching thread partially fills order)
/// - `Matched` → `Finished` (matching thread completes order)
/// So finally state is `Finished`.
/// So finally state is `Finished`.
#[derive(PartialEq, Eq, Default, Clone, Copy, Debug)]
pub enum OrderLifecycle {
/// The order is live and can be matched or canceled.
Expand Down Expand Up @@ -331,7 +331,7 @@ impl Clone for Order {
id: self.id,
user_id: self.user_id,
side: self.side,
lifecycle: AtomicU8::new(self.lifecycle.load(Ordering::Acquire).into()),
lifecycle: AtomicU8::new(self.lifecycle.load(Ordering::Acquire)),
order_type: self.order_type,
status: UnsafeCell::new(unsafe { *self.status.get() }),
match_strategy: self.match_strategy,
Expand All @@ -341,8 +341,8 @@ impl Clone for Order {
slippage_tolerance: self.slippage_tolerance,
quantity: UnsafeCell::new(unsafe { *self.quantity.get() }),
filled_quantity: UnsafeCell::new(unsafe { *self.filled_quantity.get() }),
cancel_reason: UnsafeCell::new(unsafe { (*self.cancel_reason.get()).clone() }),
reject_reason: UnsafeCell::new(unsafe { (*self.reject_reason.get()).clone() }),
cancel_reason: UnsafeCell::new(unsafe { *self.cancel_reason.get() }),
reject_reason: UnsafeCell::new(unsafe { *self.reject_reason.get() }),
created_at: self.created_at,
updated_at: self.updated_at,
}
Expand Down Expand Up @@ -387,6 +387,7 @@ impl Order {
}

/// Get the current lifecycle state is `Finished`.
#[allow(dead_code)]
#[inline(always)]
pub(crate) fn is_finished(&self) -> bool {
self.lifecycle.load(Ordering::Acquire) == OrderLifecycle::Finished.into()
Expand All @@ -402,57 +403,53 @@ impl Order {
/// Enter matched lifecycle state.
#[inline(always)]
pub(crate) fn enter_matched(&self) -> bool {
match self.lifecycle.compare_exchange_weak(
OrderLifecycle::Active.into(),
OrderLifecycle::Matched.into(),
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => true,
Err(_) => false,
}
self.lifecycle
.compare_exchange_weak(
OrderLifecycle::Active.into(),
OrderLifecycle::Matched.into(),
Ordering::AcqRel,
Ordering::Relaxed,
)
.is_ok()
}

/// Exit from matched to active lifecycle state.
#[inline(always)]
pub(crate) fn exit_matched(&self) -> bool {
match self.lifecycle.compare_exchange_weak(
OrderLifecycle::Matched.into(),
OrderLifecycle::Active.into(),
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => true,
Err(_) => false,
}
self.lifecycle
.compare_exchange_weak(
OrderLifecycle::Matched.into(),
OrderLifecycle::Active.into(),
Ordering::AcqRel,
Ordering::Relaxed,
)
.is_ok()
}

/// Enter the finished lifecycle state from active.
#[inline(always)]
pub(crate) fn enter_finished_from_active(&self) -> bool {
match self.lifecycle.compare_exchange_weak(
OrderLifecycle::Active.into(),
OrderLifecycle::Finished.into(),
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => true,
Err(_) => false,
}
self.lifecycle
.compare_exchange_weak(
OrderLifecycle::Active.into(),
OrderLifecycle::Finished.into(),
Ordering::AcqRel,
Ordering::Relaxed,
)
.is_ok()
}

/// Enter the finished lifecycle state from matched.
#[inline(always)]
pub(crate) fn enter_finished_from_matched(&self) -> bool {
match self.lifecycle.compare_exchange_weak(
OrderLifecycle::Matched.into(),
OrderLifecycle::Finished.into(),
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => true,
Err(_) => false,
}
self.lifecycle
.compare_exchange_weak(
OrderLifecycle::Matched.into(),
OrderLifecycle::Finished.into(),
Ordering::AcqRel,
Ordering::Relaxed,
)
.is_ok()
}

/// Get the order priority of the order book.
Expand Down Expand Up @@ -488,6 +485,7 @@ impl Order {
/// SAFETY:
/// Only the matching engine thread modifies cancel_reason,
/// ensuring safe access under shared reference.
#[allow(dead_code)]
#[inline(always)]
pub(crate) fn update_cancel_reason(&self, reason: CancelReason) {
unsafe {
Expand All @@ -512,11 +510,7 @@ impl Order {
/// For 'Sell' orders, the lowest price.
/// Returns `None` if no slippage tolerance is set.
pub fn slippage_bound_price(&self, price: Price) -> Option<Price> {
if self.slippage_tolerance.is_none() {
return None;
}

let slippage = self.slippage_tolerance.unwrap();
let slippage = self.slippage_tolerance?;
let mut factor = U512::from(slippage.0);
factor = factor.mul(price);
let (quotient, _) = factor.div_rem_limb_with_reciprocal(&RECIPROCAL_10000);
Expand Down
22 changes: 13 additions & 9 deletions apex-core/tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,32 @@ use apex_core::prelude::*;
use crossbeam::epoch;
use crossbeam::epoch::default_collector;
use crossbeam_skiplist::SkipList;
use std::cell::UnsafeCell;

/// Quickly generate a simple limit order for testing
pub fn make_limit_order(id: u64, side: Side, price: u64, qty: u64, ts: u64) -> Order {
let mut value = Order::default();
value.id = id;
value.user_id = 1;
value.side = side;
value.price = Price::from(price);
*value.quantity.get_mut() = Quantity::from(qty);
value.created_at = ts;
value.updated_at = ts;
value
Order {
id,
user_id: 1,
side,
price: Price::from(price),
quantity: UnsafeCell::new(Quantity::from(qty)),
created_at: ts,
updated_at: ts,
..Order::default()
}
}

/// Quickly generate a market order for testing
#[allow(dead_code)]
pub fn make_market_order(id: u64, side: Side, qty: u64, ts: u64) -> Order {
let mut value = make_limit_order(id, side, 0, qty, ts);
value.order_type = OrderType::Market;
value
}

/// Get the current state of a side of the book
#[allow(dead_code)]
pub fn get_book_state(book: &dyn OrderBookWalker, side: Side) -> Vec<(OrderID, Quantity)> {
let guard = &epoch::pin();
book.get_book(side)
Expand Down
2 changes: 1 addition & 1 deletion apex-core/tests/stress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fn test_massive_order_cancellation() {

// Randomly cancel half of them
let mut rng = rand::rng();
for i in 0..25_000 {
for _i in 0..25_000 {
let id_to_cancel = rng.random_range(0..50_000);
let _ = engine.cancel_order(id_to_cancel);
}
Expand Down
Loading