Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
40 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
034d957
chore(canister): merge main, adopt collapsed place_* order builder
gregorydemay Jun 26, 2026
0b2fb0b
Merge branch 'main' into gdemay/DEFI-2901-order-level-scalars
gregorydemay Jun 29, 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
228 changes: 114 additions & 114 deletions canister/canbench_results.yml

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions canister/oisy_trade.did
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ type OrderRecord = record {
last_updated_at : opt nat64;
/// Time-in-force policy the order was placed with.
time_in_force : TimeInForce;
/// Cumulative realized quote notional transacted across the order's fills,
/// in quote-token smallest units. A buy taker's released reservation
/// surplus is excluded; VWAP is `filled_quote × base_scale / filled_quantity`.
Comment thread
gregorydemay marked this conversation as resolved.
Outdated
filled_quote : nat;
/// Cumulative realized fee charged across the order's fills, in the order's
/// receive token — base for a buy, quote for a sell.
filled_fee : nat;
};

/// Request for `get_my_orders`.
Expand Down
45 changes: 41 additions & 4 deletions canister/src/order/history/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ pub struct OrderRecord {
/// Time-in-force policy the order was placed with.
#[n(8)]
pub time_in_force: TimeInForce,
/// Cumulative realized quote notional transacted across the order's fills,
/// `Σ (maker_price × fill_quantity / base_scale)`. Always quote-denominated;
/// a buy taker's released reservation surplus is excluded.
#[n(9)]
pub filled_quote: Quantity,
Comment thread
gregorydemay marked this conversation as resolved.
/// Cumulative realized fee charged across the order's fills, denominated in
/// the order's receive token — base for a buy, quote for a sell.
#[n(10)]
pub filled_fee: Quantity,
Comment thread
gregorydemay marked this conversation as resolved.
}

impl From<OrderRecord> for oisy_trade_types::OrderRecord {
Expand All @@ -59,17 +68,22 @@ impl From<OrderRecord> for oisy_trade_types::OrderRecord {
created_at: record.created_at.as_nanos(),
last_updated_at: record.last_updated_at.map(|t| t.as_nanos()),
time_in_force: record.time_in_force.into(),
filled_quote: record.filled_quote.into(),
filled_fee: record.filled_fee.into(),
}
}
}

/// A combined update to an order record, applied in a single read-modify-write
/// by [`OrderHistory::apply_update`]: an optional status transition plus a
/// fill delta to add to `filled_quantity`.
/// by [`OrderHistory::apply_update`]: an optional status transition plus the
/// fill, quote, and fee deltas to add to `filled_quantity`, `filled_quote`, and
/// `filled_fee`.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct OrderUpdate {
pub status: Option<OrderStatus>,
pub filled_delta: Quantity,
pub quote_delta: Quantity,
pub fee_delta: Quantity,
}

impl OrderUpdate {
Expand All @@ -78,6 +92,8 @@ impl OrderUpdate {
Self {
status: Some(status),
filled_delta: Quantity::ZERO,
quote_delta: Quantity::ZERO,
fee_delta: Quantity::ZERO,
}
}

Expand All @@ -86,6 +102,8 @@ impl OrderUpdate {
Self {
status: None,
filled_delta,
quote_delta: Quantity::ZERO,
fee_delta: Quantity::ZERO,
}
}

Expand All @@ -94,13 +112,16 @@ impl OrderUpdate {
/// # Panics
///
/// `filled_quantity` is monotonic non-decreasing and must never exceed
/// `quantity`; this invariant is enforced by an always-on check that traps
/// on violation.
/// `quantity`; `filled_quote` and `filled_fee` are monotonic
/// non-decreasing. These invariants are enforced by always-on checks that
/// trap on violation.
pub fn apply(self, order: &mut OrderRecord) -> bool {
let mut changed = false;
let OrderUpdate {
status,
filled_delta,
quote_delta,
fee_delta,
} = self;

if let Some(new_status) = status
Expand All @@ -125,6 +146,22 @@ impl OrderUpdate {
order.created_at,
);
}

if quote_delta != Quantity::ZERO {
changed = true;
order.filled_quote = order
.filled_quote
.checked_add(quote_delta)
.expect("BUG: filled_quote overflow");
}

if fee_delta != Quantity::ZERO {
changed = true;
order.filled_fee = order
.filled_fee
.checked_add(fee_delta)
.expect("BUG: filled_fee overflow");
}
changed
}
}
Expand Down
70 changes: 69 additions & 1 deletion canister/src/order/history/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ fn test_record() -> OrderRecord {
created_at: Timestamp::EPOCH,
last_updated_at: None,
time_in_force: TimeInForce::FillOrKill,
filled_quote: Quantity::ZERO,
filled_fee: Quantity::ZERO,
}
}

Expand Down Expand Up @@ -134,6 +136,8 @@ fn apply_update_status_and_delta_in_one_write() {
OrderUpdate {
status: Some(OrderStatus::Open),
filled_delta: Quantity::from(300_000u64),
quote_delta: Quantity::from(3u64),
fee_delta: Quantity::from(1u64),
},
Timestamp::new(11),
);
Expand All @@ -142,16 +146,80 @@ fn apply_update_status_and_delta_in_one_write() {
OrderUpdate {
status: Some(OrderStatus::Filled),
filled_delta: Quantity::from(700_000u64),
quote_delta: Quantity::from(7u64),
fee_delta: Quantity::from(2u64),
},
Timestamp::new(13),
);
let record = history.get(&id).expect("record present");
// Deltas accumulate; status reflects the latest update.
// The fill, quote, and fee deltas all accumulate within the same single
// read-modify-write; status reflects the latest update.
assert_eq!(record.status, OrderStatus::Filled);
assert_eq!(record.filled_quantity, Quantity::from(1_000_000u64));
assert_eq!(record.filled_quote, Quantity::from(10u64));
assert_eq!(record.filled_fee, Quantity::from(3u64));
assert_eq!(record.last_updated_at, Some(Timestamp::new(13)));
}

#[test]
fn apply_update_accumulates_quote_and_fee_in_one_write() {
let mut history = history();
let id = order_id(0);
history.insert_once(UserId::new(0), id, test_record());

// A fill-only update that carries quote and fee deltas writes all three
// scalars (and `last_updated_at`) in a single read-modify-write, leaving
// status untouched.
history.apply_update(
&id,
OrderUpdate {
status: None,
filled_delta: Quantity::from(200_000u64),
quote_delta: Quantity::from(20_000_000u64),
fee_delta: Quantity::from(10_000u64),
},
Timestamp::new(5),
);
let record = history.get(&id).expect("record present");
assert_eq!(record.status, OrderStatus::Pending);
assert_eq!(record.filled_quantity, Quantity::from(200_000u64));
assert_eq!(record.filled_quote, Quantity::from(20_000_000u64));
assert_eq!(record.filled_fee, Quantity::from(10_000u64));
assert_eq!(record.last_updated_at, Some(Timestamp::new(5)));
}

#[test]
#[should_panic(expected = "BUG: filled_quote overflow")]
fn apply_update_traps_on_filled_quote_overflow() {
// The monotonic `filled_quote` invariant is enforced by an always-on trap,
// not a `debug_assert!` compiled out of the release canister: starting from
// `Quantity::MAX`, any positive `quote_delta` overflows and must panic even
// when tests run in release config.
let mut record = test_record();
record.filled_quote = Quantity::MAX;
OrderUpdate {
status: None,
filled_delta: Quantity::ZERO,
quote_delta: Quantity::from(1u64),
fee_delta: Quantity::ZERO,
}
.apply(&mut record);
}

#[test]
#[should_panic(expected = "BUG: filled_fee overflow")]
fn apply_update_traps_on_filled_fee_overflow() {
let mut record = test_record();
record.filled_fee = Quantity::MAX;
OrderUpdate {
status: None,
filled_delta: Quantity::ZERO,
quote_delta: Quantity::ZERO,
fee_delta: Quantity::from(1u64),
}
.apply(&mut record);
}

#[test]
fn apply_update_is_a_noop_when_update_is_a_noop() {
let mut history = history();
Expand Down
Loading
Loading