Skip to content
Merged
Show file tree
Hide file tree
Changes from 136 commits
Commits
Show all changes
137 commits
Select commit Hold shift + click to select a range
4ce3c35
feat(order): roll up realized quote and fee onto OrderRecord
gregorydemay Jun 23, 2026
a86e05d
test(order): label worked-example fixture as ckBTC, note spec uses ck…
gregorydemay Jun 23, 2026
17109c5
perf(state): settle fills in a single pass without an intermediate Vec
gregorydemay Jun 23, 2026
e71fe50
chore(canister): refresh canbench baseline for order-level scalars
gregorydemay Jun 23, 2026
0efe8e5
test(canister): derive arb_order_record notional via base_scale
gregorydemay Jun 23, 2026
1128a86
test(canister): make realized_scalars a literal ICP/ckUSDT example
gregorydemay Jun 23, 2026
2198311
docs(candid): simplify filled_quote/filled_fee field docs
gregorydemay Jun 23, 2026
6b4a9dd
refactor(state): wrap Fill in FillSettlement, single write gate, rena…
gregorydemay Jun 23, 2026
c730732
test(state): fold realized-scalar coverage into existing fee tests
gregorydemay Jun 23, 2026
e97d66c
test(int): exercise non-zero fees in partial-fill cancel test
gregorydemay Jun 23, 2026
61abf99
chore(canister): refresh canbench baseline for renamed scope
gregorydemay Jun 23, 2026
d08b235
Merge remote-tracking branch 'origin/main' into gdemay/DEFI-2901-orde…
gregorydemay Jun 23, 2026
ccd8246
Merge remote-tracking branch 'origin/main' into gdemay/DEFI-2901-orde…
gregorydemay Jun 24, 2026
f90f243
chore(canister): refresh canbench baseline after main merge
gregorydemay Jun 24, 2026
df627ae
test(state): rename proptest to reflect fill_settlement + push_balanc…
gregorydemay Jun 24, 2026
4abdc5c
docs(did): express order VWAP as filled_quote / filled_quantity
gregorydemay Jun 24, 2026
15e624e
refactor(state): have FillSettlement own its Fill
gregorydemay Jun 24, 2026
9767bfe
docs(types): align filled_quote VWAP docstring with the .did (drop ba…
gregorydemay Jun 24, 2026
e632896
refactor(order): promote FillSettlement to a first-class type
gregorydemay Jun 25, 2026
8860030
Merge remote-tracking branch 'origin/main' into gdemay/DEFI-2901-orde…
gregorydemay Jun 25, 2026
b1b9d56
DEFI-2901: refactor accrue_fill
gregorydemay Jun 25, 2026
20e5b7e
DEFI-2901: refactor FillSettlement
gregorydemay Jun 25, 2026
67700b9
DEFI-2901: remove dead code
gregorydemay Jun 25, 2026
a23bf6e
DEFI-2901: simplify comments
gregorydemay Jun 25, 2026
738a76e
DEFI-2901: remove unnecessary clone
gregorydemay Jun 25, 2026
01ec74c
refactor(canister): encapsulate balance-op construction in a settle()…
gregorydemay Jun 25, 2026
3b664d3
chore(canister): regenerate canbench results after settle() refactor
gregorydemay Jun 25, 2026
c2e8354
perf(canister): gate full settlement under the write gate during replay
gregorydemay Jun 25, 2026
c427980
test(canister): drop review-flagged comments in state tests
gregorydemay Jun 25, 2026
3099210
test(canister): drop duplicate ckUSDT metadata fixture
gregorydemay Jun 25, 2026
c2fa150
test(canister): drive realized-scalar examples from a TestCase table
gregorydemay Jun 25, 2026
f676083
DEFI-2901: complete expectations
gregorydemay Jun 25, 2026
d1d23b4
test(canister): move record_of helper into test_fixtures
gregorydemay Jun 25, 2026
f0e21a0
test(canister): assert filled records via assert_eq_ignoring_timestamp
gregorydemay Jun 25, 2026
933e131
test(canister): drive worked-example matching with mock_runtime_for_t…
gregorydemay Jun 25, 2026
772edb2
perf(canister): gate cancel settlement under the write gate during re…
gregorydemay Jun 25, 2026
3fc7aed
test(canister): build token-id fixtures via SupportedTokens::token_id
gregorydemay Jun 26, 2026
e28d073
test(int): drop redundant comment in cancel partial-fill refund test
gregorydemay Jun 26, 2026
aed1cf5
feat(order): persist per-fill records in stable memory (2/4)
gregorydemay Jun 26, 2026
c5b4f8f
test(canister): restore the shared test_fixtures::record_of helper
gregorydemay Jun 26, 2026
075b728
test(canister): build token-id fixtures via SupportedTokens::token_id
gregorydemay Jun 26, 2026
953de4c
test(canister): drop redundant fill-persistence bench, reuse gtc full…
gregorydemay Jun 26, 2026
36836bc
fix(canister): keep cancel-path settlement under the write gate
gregorydemay Jun 26, 2026
034d957
chore(canister): merge main, adopt collapsed place_* order builder
gregorydemay Jun 26, 2026
b00db78
chore(canister): merge order-level-scalars, adopt collapsed order bui…
gregorydemay Jun 26, 2026
4e078a4
refactor(order): promote FillKey into a first-class FillId identifier
gregorydemay Jun 26, 2026
d578ff8
feat(order): index fills by account and write the by_user index on se…
gregorydemay Jun 26, 2026
1bc7db4
docs(specs): adopt FillId identity and fold ByAccount storage into PR-2
gregorydemay Jun 26, 2026
9595d76
refactor(order): adopt the denormalized Fill/Trade identity model
gregorydemay Jun 26, 2026
998e31b
docs(specs): rewrite DEFI-2901 to the denormalized Trade model
gregorydemay Jun 26, 2026
ce05e4d
test(canister): refresh canbench baseline for the trade store
gregorydemay Jun 26, 2026
9840e4c
perf(order): resolve the trade account cursor in O(log n)
gregorydemay Jun 26, 2026
d2e215e
refactor(order): drop the unused OrderBook::next_fill accessor
gregorydemay Jun 26, 2026
680530b
perf(state): share the cached order-owner resolution for fill records
gregorydemay Jun 26, 2026
e2752d5
perf(state): skip owner-cache build when a matching round has no fills
gregorydemay Jun 26, 2026
00c98c9
Merge origin/main into gdemay/DEFI-2901-fill-store
gregorydemay Jun 29, 2026
cb7adbf
chore(canister): drop retention/pruning TODO in TradeHistory::insert
gregorydemay Jun 29, 2026
fb32e97
DEFI-2901: Seq generic type
gregorydemay Jun 29, 2026
6e0b36d
DEFI-2901: debug Seq
gregorydemay Jun 29, 2026
8cd8124
feat(order): add per-book fill sequence to Fill
gregorydemay Jun 29, 2026
b002b02
refactor(order): migrate OrderBookId onto the Seq generic
gregorydemay Jun 29, 2026
884ccd8
DEFI-2901: composite ID
gregorydemay Jun 29, 2026
e0c8b5c
refactor(ids): promote ids to a top-level module
gregorydemay Jun 29, 2026
67c27de
test(ids): add proptest strategies and id roundtrip helpers
gregorydemay Jun 29, 2026
886c44d
test(ids): cover nested CompositeId round-trips
gregorydemay Jun 29, 2026
ab22f73
chore(canister): regenerate canbench results and fix doc typo
gregorydemay Jun 29, 2026
f9ace5a
Merge remote-tracking branch 'origin/gdemay/DEFI-2901-id-types' into …
gregorydemay Jun 29, 2026
7e76888
chore(canister): regenerate canbench results after id-layer merge
gregorydemay Jun 29, 2026
7e109df
refactor(canister): fold TradeByUserKey onto the shared CompositeId l…
gregorydemay Jun 29, 2026
2762846
test(canister): drop the self-asserting no-counterparty fixture test
gregorydemay Jun 29, 2026
7b03ac5
refactor(user): make UserId a Seq alias
gregorydemay Jun 30, 2026
24e72e6
Merge gdemay/DEFI-2901-id-types into gdemay/DEFI-2901-fill-store
gregorydemay Jun 30, 2026
d689aaa
test(order): drop generic id tests from the fills suite
gregorydemay Jun 30, 2026
1dca2f8
refactor(fills): fold TradeByUserKey constructors into impl methods
gregorydemay Jun 30, 2026
9a30552
refactor(fills): rename Trade to TradeRecord
gregorydemay Jun 30, 2026
5a5b965
test(fills): add arbitrary TradeRecord strategy and minicbor roundtrip
gregorydemay Jun 30, 2026
46c5083
refactor(fills): drop the unused Storable impl for TradeRecord
gregorydemay Jun 30, 2026
9b1e980
refactor(order): rename the fills module to trades
gregorydemay Jun 30, 2026
94bb858
refactor(canister): rename the trade store binding to trade_history
gregorydemay Jun 30, 2026
46d2bb5
refactor(order): let TradeHistory::append consume a FillSettlement
gregorydemay Jun 30, 2026
1fbb070
fix(canister): reject non-canonical hex in FixedWidthId::from_hex
gregorydemay Jun 30, 2026
7b8d8e5
style(canister): apply cargo fmt after append-refactor
gregorydemay Jun 30, 2026
a8a4426
Merge remote-tracking branch 'origin/gdemay/DEFI-2901-id-types' into …
gregorydemay Jun 30, 2026
365ce0e
refactor(canister): share canonical-hex validation across from_hex impls
gregorydemay Jun 30, 2026
48f2275
Merge remote-tracking branch 'origin/gdemay/DEFI-2901-id-types' into …
gregorydemay Jun 30, 2026
e031c66
test(order): drive TradeHistory store tests through append(FillSettle…
gregorydemay Jun 30, 2026
cafb9ce
refactor(canister): use matches! for canonical-hex byte check
gregorydemay Jun 30, 2026
7bc2be1
Merge remote-tracking branch 'origin/gdemay/DEFI-2901-id-types' into …
gregorydemay Jun 30, 2026
ce18afd
Merge remote-tracking branch 'origin/main' into gdemay/DEFI-2901-fill…
gregorydemay Jun 30, 2026
e4b4725
chore(canister): refresh canbench results after merging main
gregorydemay Jun 30, 2026
f68d8e1
docs(specs): align persist-fills spec with shipped trades module
gregorydemay Jun 30, 2026
1276542
refactor(canister): rename SeqTrade to SeqTradeRecord, type global_se…
gregorydemay Jun 30, 2026
dc561fb
refactor(canister): move trade persistence into the settling phase
gregorydemay Jun 30, 2026
1ef2b40
perf(canister): refresh canbench after moving persistence to settling
gregorydemay Jun 30, 2026
0f00cfc
refactor(canister): shrink the matching→settling event boundary to a …
gregorydemay Jun 30, 2026
7c45bf2
perf(canister): refresh canbench after leaning the settling event bou…
gregorydemay Jun 30, 2026
9e3b512
refactor(canister): drop unused FillSettlement derives
gregorydemay Jun 30, 2026
b1cd62b
test(canister): cover the Skip gate in record_settling_event
gregorydemay Jun 30, 2026
f5e38dc
test(canister): make worst-case Settling fixture genuinely max-width
gregorydemay Jun 30, 2026
ca41fbd
docs(specs): rename stored trade value to SeqTradeRecord
gregorydemay Jun 30, 2026
75f98db
perf(canister): compute fill notional once per call site
gregorydemay Jun 30, 2026
1619137
feat(order): add the trade store and its read API (1/5)
gregorydemay Jun 30, 2026
fa456ee
Merge remote-tracking branch 'origin/gdemay/DEFI-2901-trade-store' in…
gregorydemay Jun 30, 2026
7cf47ed
test(state): cover owner-scoping of get_user_order_trades
gregorydemay Jun 30, 2026
6a8266c
test(order): collapse trade-append side wrappers into append
gregorydemay Jun 30, 2026
e695cc6
docs(order): correct trades_after complexity to O(length * log n)
gregorydemay Jun 30, 2026
bcf5222
feat(history): add the generic History store and build TradeHistory o…
gregorydemay Jun 30, 2026
8db56ee
test(history): cover by-user paging; drop TradeHistory's account-wide…
gregorydemay Jun 30, 2026
67d550b
refactor(order): build OrderHistory on the shared History core
gregorydemay Jun 30, 2026
8dbb2c8
test(history): cover insert/get/dup; drop OrderHistory's equivalents
gregorydemay Jun 30, 2026
e81830e
test(history): cover read-modify-write; drop OrderHistory's noop-upda…
gregorydemay Jun 30, 2026
8eba2fc
test(order): drop orders_after tests covered by the generic paging suite
gregorydemay Jun 30, 2026
3baed4c
test: drop store tests redundant with the generic and state suites
gregorydemay Jun 30, 2026
e732f21
docs(canister): refer to History's primary/by-user maps in trade memo…
gregorydemay Jun 30, 2026
e910250
docs(canister): clarify History insertion sequence is per-store, not …
gregorydemay Jul 1, 2026
1634848
refactor(canister): type History insertion seq as HistoryGlobalSeq, r…
gregorydemay Jul 1, 2026
f5fa966
refactor(canister): relax History key bound from Copy to Clone
gregorydemay Jul 1, 2026
3a8b988
refactor(canister): rename HistoryGlobalSeq to InsertionSeq
gregorydemay Jul 1, 2026
385d66c
refactor(canister): rename SeqEnvelope to SeqRecord
gregorydemay Jul 1, 2026
fb3daa5
test(canister): table-drive range_primary history test
gregorydemay Jul 1, 2026
fe20fd4
test(canister): consolidate trades_for_order tests into a table
gregorydemay Jul 1, 2026
7e5bd15
refactor(canister): defer the per-order trade reader out of the trade…
gregorydemay Jul 1, 2026
932856f
Merge remote-tracking branch 'origin/gdemay/DEFI-2901-trade-store' in…
gregorydemay Jul 1, 2026
6716e6d
docs(canister): trim the verbose SettledFill reprice/exchange note
gregorydemay Jul 1, 2026
c2073ee
refactor(canister): drop unused Encode/Decode from Fill
gregorydemay Jul 1, 2026
a575301
refactor(canister): take SettledFill by value in trade_legs
gregorydemay Jul 1, 2026
3c6c147
refactor(canister): rename SettledFill to FillEvent
gregorydemay Jul 1, 2026
5df1307
feat(canister): expose per-fill data on the candid Settling event
gregorydemay Jul 1, 2026
e3f4238
Merge remote-tracking branch 'origin/main' into gdemay/DEFI-2901-fill…
gregorydemay Jul 1, 2026
41c3c77
refactor(canister): rename resolve_op_users to resolve_op_orders
gregorydemay Jul 1, 2026
fc67d50
refactor(canister): drop unused TradeCursorNotFound re-export
gregorydemay Jul 1, 2026
b19c71a
refactor(canister): compute trade-leg fees without a fabricated Fill
gregorydemay Jul 1, 2026
717381a
refactor(canister): make settling order-seq lookup panic explicit
gregorydemay Jul 1, 2026
41a90ab
test(canister): assert trade legs as whole records, not field-by-field
gregorydemay Jul 1, 2026
a2bc218
test(canister): drop requirement-referencing doc comment on skip-gate…
gregorydemay Jul 1, 2026
899f751
test(canister): assert per-fill settlement records as whole records
gregorydemay Jul 1, 2026
324cdb5
docs(canister): describe both side effects of record_settling_event
gregorydemay Jul 1, 2026
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
264 changes: 152 additions & 112 deletions canister/canbench_results.yml

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions canister/oisy_trade.did
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,18 @@ type MatchingEvent = record {
type SettlingEvent = record {
book_id : nat64;
balance_operations : vec BalanceOperation;
fills : vec FillEvent;
};

// Per-fill record carried on a settling event: the fill's identity, the matched
// quantity, and the maker/taker fee rates in basis points.
type FillEvent = record {
fill_seq : nat64;
taker_order_seq : nat64;
maker_order_seq : nat64;
quantity : nat;
maker_fee_bps : nat16;
taker_fee_bps : nat16;
};

// Balance movement recorded during settlement. `from_order` / `to_order` /
Expand Down
13 changes: 13 additions & 0 deletions canister/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,17 @@ fn get_events(
}
}

fn map_fill_event(fill: oisy_trade_canister::order::FillEvent) -> event::FillEvent {
event::FillEvent {
fill_seq: fill.fill_seq.get(),
taker_order_seq: fill.taker_order_seq.get(),
maker_order_seq: fill.maker_order_seq.get(),
quantity: fill.quantity.into(),
maker_fee_bps: fill.fee_rates.maker.get(),
taker_fee_bps: fill.fee_rates.taker.get(),
}
}

fn map_event(event: Event) -> event::Event {
event::Event {
timestamp: event.timestamp.as_nanos(),
Expand Down Expand Up @@ -333,12 +344,14 @@ fn get_events(
EventType::Settling(oisy_trade_canister::state::event::SettlingEvent {
book_id,
balance_operations,
fills,
}) => event::EventType::Settling(event::SettlingEvent {
book_id: book_id.get(),
balance_operations: balance_operations
.into_iter()
.map(map_balance_operation)
.collect(),
fills: fills.into_iter().map(map_fill_event).collect(),
}),
EventType::SetHalt(e) => event::EventType::SetHalt(event::SetHaltEvent {
book_ids: e
Expand Down
187 changes: 151 additions & 36 deletions canister/src/order/fill.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use super::{
FeeRates, OrderBookId, OrderId, OrderSeq, OrderUpdate, PairToken, Price, Quantity,
RemovedOrder, Side,
RemovedOrder, Side, TradeLeg, TradeRecord,
};
use crate::Timestamp;
use crate::ids::{CompositeId, Seq, SeqMarker};
use crate::state::event;
use minicbor::{Decode, Encode};
Expand Down Expand Up @@ -49,50 +50,42 @@ impl TradeId {
}

/// A single fill produced when an incoming order matches a resting order.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fill {
/// The per-book sequence of this match, minted by the order book.
#[n(0)]
pub fill_seq: FillSeq,
/// The sequence of the incoming (taker) order.
#[n(1)]
pub taker_order_seq: OrderSeq,
/// The side of the taker order.
#[n(2)]
pub taker_side: Side,
/// The limit price of the taker order.
#[n(3)]
pub taker_price: Price,
/// The sequence of the resting (maker) order that was matched.
#[n(4)]
pub maker_order_seq: OrderSeq,
/// The price at which the fill occurred (always the maker's price).
#[n(5)]
pub maker_price: Price,
/// The quantity filled.
#[n(6)]
pub quantity: Quantity,
}

impl Fill {
/// The amount of quote tokens exchanged:
/// `maker_price × quantity / base_scale` (`base_scale = 10^base_decimals`).
pub fn quote_amount(&self, base_scale: NonZeroU64) -> Quantity {
self.maker_price
.checked_mul_quantity_scaled(&self.quantity, base_scale)
.expect("BUG: validation of order should prevent overflow")
}

/// The amount of base tokens exchanged (same as quantity).
pub fn base_amount(&self) -> &Quantity {
&self.quantity
}
}

/// A single [`Fill`] together with the realized values derived from it, computed
/// once in settlement (the only point where both `fee_rates` and `base_scale`
/// are in scope) and reused to build both the [`event::BalanceOperation`]s and
/// the per-order scalar deltas, so the two can never diverge.
/// once in the matching phase (the only point where both `fee_rates` and
/// `base_scale` are in scope) and used to project both the
/// [`event::BalanceOperation`]s and the per-order scalar deltas, so the two can
/// never diverge.
///
/// This is a matching-phase, heap-only helper: it is never CBOR-encoded into the
/// event log. The settling event carries the lean [`FillEvent`] instead, and the
/// settling phase recovers side/price from the order records and recomputes the
/// realized values to rebuild the two side-projected [`TradeRecord`]s.
#[derive(Debug)]
pub struct FillSettlement {
fill: Fill,
/// Quote notional `maker_price × quantity / base_scale` (the executed
Expand All @@ -106,27 +99,21 @@ pub struct FillSettlement {
/// Quote surplus released back to a buy taker that crossed below its limit;
/// Zero for a sell taker or an exact-price fill.
surplus: Quantity,
/// Snapshot of the book's fee rates at match time, carried onto the lean
/// [`FillEvent`] so settling can recompute the fees off the pinned rates.
fee_rates: FeeRates,
}

impl FillSettlement {
/// Compute the realized values of a single fill once.
pub fn new(fill: Fill, fee_rates: FeeRates, base_scale: NonZeroU64) -> Self {
// Receive-side convention: buyer pays fee in base (the asset they
// receive), seller in quote. Each side's rate is `taker` if they
// were the taker, else `maker`.
let (buyer_rate, seller_rate) = match fill.taker_side {
Side::Buy => (fee_rates.taker, fee_rates.maker),
Side::Sell => (fee_rates.maker, fee_rates.taker),
};
let notional = fill.quote_amount(base_scale);
let quote_fee = seller_rate.mul_ceil(notional);
let base_fee = buyer_rate.mul_ceil(fill.quantity);
// The taker pays on the side it traded: base if it bought, quote if
// it sold. The maker pays on the opposite side.
let (taker_fee, maker_fee) = match fill.taker_side {
Side::Buy => (base_fee, quote_fee),
Side::Sell => (quote_fee, base_fee),
};
let (notional, taker_fee, maker_fee) = fees(
fill.maker_price,
fill.quantity,
fill.taker_side,
fee_rates,
base_scale,
);
let surplus = if fill.taker_side == Side::Buy
&& let Some(diff) = fill.taker_price.checked_sub(fill.maker_price)
&& !diff.is_zero()
Expand All @@ -142,6 +129,7 @@ impl FillSettlement {
taker_fee,
maker_fee,
surplus,
fee_rates,
}
}

Expand Down Expand Up @@ -200,6 +188,124 @@ impl FillSettlement {
.expect("BUG: fee_delta overflow");
}
}

/// The lean, normalized record persisted on the settling event: the fill's
/// identity, quantity, and the fee-rate snapshot — everything else is
/// recovered or recomputed in the settling phase.
pub fn settled_fill(&self) -> FillEvent {
FillEvent {
fill_seq: self.fill.fill_seq,
taker_order_seq: self.fill.taker_order_seq,
maker_order_seq: self.fill.maker_order_seq,
quantity: self.fill.quantity,
fee_rates: self.fee_rates,
}
}
}

/// The lean, normalized per-fill record carried on the settling event and the only
/// fill data persisted in the event log. It stores just what cannot be recovered
/// otherwise: the fill's identity, the matched `quantity`, and a snapshot of the
/// book's `fee_rates` at match time.
///
/// The fill's execution price (the maker price) and its taker `side` are NOT
/// stored: they are recovered in the settling phase from the two referenced order
/// records. `fee_rates` is snapshotted here rather than recovered because the rate
/// lives on the book and is mutable — it is the one fee input pinned by neither the
/// fill identity nor the orders.
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
pub struct FillEvent {
#[n(0)]
pub fill_seq: FillSeq,
#[n(1)]
pub taker_order_seq: OrderSeq,
#[n(2)]
pub maker_order_seq: OrderSeq,
#[n(3)]
pub quantity: Quantity,
#[n(4)]
pub fee_rates: FeeRates,
}

impl FillEvent {
/// Rebuild the two side-projected [`TradeRecord`]s — the taker leg and the
/// maker leg — from this lean record, the side/price recovered from the order
/// records, and the freshly recomputed `notional` and fees. Each leg is keyed
/// by its [`TradeId`] `(OrderId, FillSeq)`, shares the match's `fill_seq`, and
/// self-describes one order's view of the execution without ever referencing
/// the counterparty. Consumed by [`crate::order::TradeHistory::append`].
pub fn trade_legs(
&self,
Comment thread
gregorydemay marked this conversation as resolved.
book_id: OrderBookId,
taker_side: Side,
maker_price: Price,
base_scale: NonZeroU64,
timestamp: Timestamp,
) -> [TradeLeg; 2] {
let (notional, taker_fee, maker_fee) = fees(
maker_price,
self.quantity,
taker_side,
self.fee_rates,
base_scale,
);
let maker_side = match taker_side {
Side::Buy => Side::Sell,
Side::Sell => Side::Buy,
};
let taker_id = TradeId::new(OrderId::new(book_id, self.taker_order_seq), self.fill_seq);
let taker_leg = TradeRecord {
side: taker_side,
price: maker_price,
quantity: self.quantity,
notional,
fee: taker_fee,
fee_token: fee_token(taker_side),
is_maker: false,
timestamp,
};
let maker_id = TradeId::new(OrderId::new(book_id, self.maker_order_seq), self.fill_seq);
let maker_leg = TradeRecord {
side: maker_side,
price: maker_price,
quantity: self.quantity,
notional,
fee: maker_fee,
fee_token: fee_token(maker_side),
is_maker: true,
timestamp,
};
[(taker_id, taker_leg), (maker_id, maker_leg)]
}
}

/// The fill's `(notional, taker_fee, maker_fee)`: the quote notional and the fee
/// charged to each of the fill's two orders, each in its receive token. Shared by
/// the matching-phase [`FillSettlement::new`] and the settling-phase
/// [`FillEvent::trade_legs`] recompute, so the balance ops and the persisted trade
/// legs can never diverge, and the notional (the costliest arithmetic) is computed
/// once per fill at each call site.
fn fees(
Comment thread
gregorydemay marked this conversation as resolved.
maker_price: Price,
quantity: Quantity,
taker_side: Side,
fee_rates: FeeRates,
base_scale: NonZeroU64,
) -> (Quantity, Quantity, Quantity) {
let (buyer_rate, seller_rate) = match taker_side {
Side::Buy => (fee_rates.taker, fee_rates.maker),
Side::Sell => (fee_rates.maker, fee_rates.taker),
};
let notional = maker_price
.checked_mul_quantity_scaled(&quantity, base_scale)
.expect("BUG: validation of order should prevent overflow");
let quote_fee = seller_rate.mul_ceil(notional);
let base_fee = buyer_rate.mul_ceil(quantity);
let (taker_fee, maker_fee) = match taker_side {
Side::Buy => (base_fee, quote_fee),
Side::Sell => (quote_fee, base_fee),
};
(notional, taker_fee, maker_fee)
}

/// The settlement of a removed order (canceled or killed): the placement
Expand Down Expand Up @@ -241,6 +347,15 @@ impl RemovedOrderSettlement {
}
}

/// The token a fill's fee is charged in, per the receive-side convention: a
/// buyer is charged in the base token it receives, a seller in quote.
fn fee_token(side: Side) -> PairToken {
match side {
Side::Buy => PairToken::Base,
Side::Sell => PairToken::Quote,
}
}

/// Collapse a zero-quantity fee to `None`. Keeps `Some(_)` reserved for
/// "fee was actually charged" so callers (audit log, apply path,
/// `/metrics`) can distinguish "no fee on this fill" from "fee of zero
Expand Down
3 changes: 1 addition & 2 deletions canister/src/order/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ mod tests;
mod trades;

pub use crate::history::CursorNotFound;
pub use crate::history::CursorNotFound as TradeCursorNotFound;
pub use book::{
MatchOrderError, MatchResult, MatchingOutput, NotionalError, OrderBook, OrderBookSnapshot,
PriceLevel, RemovedOrder,
};
pub use fees::{BasisPoint, FeeRates, InvalidBasisPoint};
pub use fill::{Fill, FillId, FillSeq, FillSettlement, RemovedOrderSettlement, TradeId};
pub use fill::{Fill, FillEvent, FillId, FillSeq, FillSettlement, RemovedOrderSettlement, TradeId};
pub use history::{OrderHistory, OrderUpdate};
pub use trades::{TradeHistory, TradeLeg, TradeRecord};

Expand Down
Loading
Loading