-
Notifications
You must be signed in to change notification settings - Fork 0
feat(order): add realized quote and fee onto OrderRecord #171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all 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 a86e05d
test(order): label worked-example fixture as ckBTC, note spec uses ck…
gregorydemay 17109c5
perf(state): settle fills in a single pass without an intermediate Vec
gregorydemay e71fe50
chore(canister): refresh canbench baseline for order-level scalars
gregorydemay 0efe8e5
test(canister): derive arb_order_record notional via base_scale
gregorydemay 1128a86
test(canister): make realized_scalars a literal ICP/ckUSDT example
gregorydemay 2198311
docs(candid): simplify filled_quote/filled_fee field docs
gregorydemay 6b4a9dd
refactor(state): wrap Fill in FillSettlement, single write gate, rena…
gregorydemay c730732
test(state): fold realized-scalar coverage into existing fee tests
gregorydemay e97d66c
test(int): exercise non-zero fees in partial-fill cancel test
gregorydemay 61abf99
chore(canister): refresh canbench baseline for renamed scope
gregorydemay d08b235
Merge remote-tracking branch 'origin/main' into gdemay/DEFI-2901-orde…
gregorydemay ccd8246
Merge remote-tracking branch 'origin/main' into gdemay/DEFI-2901-orde…
gregorydemay f90f243
chore(canister): refresh canbench baseline after main merge
gregorydemay df627ae
test(state): rename proptest to reflect fill_settlement + push_balanc…
gregorydemay 4abdc5c
docs(did): express order VWAP as filled_quote / filled_quantity
gregorydemay 15e624e
refactor(state): have FillSettlement own its Fill
gregorydemay 9767bfe
docs(types): align filled_quote VWAP docstring with the .did (drop ba…
gregorydemay e632896
refactor(order): promote FillSettlement to a first-class type
gregorydemay 8860030
Merge remote-tracking branch 'origin/main' into gdemay/DEFI-2901-orde…
gregorydemay b1b9d56
DEFI-2901: refactor accrue_fill
gregorydemay 20e5b7e
DEFI-2901: refactor FillSettlement
gregorydemay 67700b9
DEFI-2901: remove dead code
gregorydemay a23bf6e
DEFI-2901: simplify comments
gregorydemay 738a76e
DEFI-2901: remove unnecessary clone
gregorydemay 01ec74c
refactor(canister): encapsulate balance-op construction in a settle()…
gregorydemay 3b664d3
chore(canister): regenerate canbench results after settle() refactor
gregorydemay c2e8354
perf(canister): gate full settlement under the write gate during replay
gregorydemay c427980
test(canister): drop review-flagged comments in state tests
gregorydemay 3099210
test(canister): drop duplicate ckUSDT metadata fixture
gregorydemay c2fa150
test(canister): drive realized-scalar examples from a TestCase table
gregorydemay f676083
DEFI-2901: complete expectations
gregorydemay d1d23b4
test(canister): move record_of helper into test_fixtures
gregorydemay f0e21a0
test(canister): assert filled records via assert_eq_ignoring_timestamp
gregorydemay 933e131
test(canister): drive worked-example matching with mock_runtime_for_t…
gregorydemay 772edb2
perf(canister): gate cancel settlement under the write gate during re…
gregorydemay 3fc7aed
test(canister): build token-id fixtures via SupportedTokens::token_id
gregorydemay e28d073
test(int): drop redundant comment in cancel partial-fill refund test
gregorydemay 034d957
chore(canister): merge main, adopt collapsed place_* order builder
gregorydemay 0b2fb0b
Merge branch 'main' into gdemay/DEFI-2901-order-level-scalars
gregorydemay File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| use super::{FeeRates, OrderSeq, OrderUpdate, PairToken, Price, Quantity, RemovedOrder, Side}; | ||
| use crate::state::event; | ||
| use minicbor::{Decode, Encode}; | ||
| use std::collections::BTreeMap; | ||
| use std::num::NonZeroU64; | ||
|
|
||
| /// A single fill produced when an incoming order matches a resting order. | ||
| #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] | ||
| pub struct Fill { | ||
| /// The sequence of the incoming (taker) order. | ||
| #[n(0)] | ||
| pub taker_order_seq: OrderSeq, | ||
| /// The side of the taker order. | ||
| #[n(1)] | ||
| pub taker_side: Side, | ||
| /// The limit price of the taker order. | ||
| #[n(2)] | ||
| pub taker_price: Price, | ||
| /// The sequence of the resting (maker) order that was matched. | ||
| #[n(3)] | ||
| pub maker_order_seq: OrderSeq, | ||
| /// The price at which the fill occurred (always the maker's price). | ||
| #[n(4)] | ||
| pub maker_price: Price, | ||
| /// The quantity filled. | ||
| #[n(5)] | ||
| 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. | ||
| pub struct FillSettlement { | ||
| fill: Fill, | ||
| /// Quote notional `maker_price × quantity / base_scale` (the executed | ||
| /// price; a buy taker's reservation surplus is excluded). | ||
| notional: Quantity, | ||
| /// Fee charged to the taker order, in its receive token (base if the taker | ||
| /// bought, quote if it sold). | ||
| taker_fee: Quantity, | ||
| /// Fee charged to the maker order, in its receive token. | ||
| maker_fee: Quantity, | ||
| /// 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, | ||
| } | ||
|
|
||
| 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 surplus = if fill.taker_side == Side::Buy | ||
| && let Some(diff) = fill.taker_price.checked_sub(fill.maker_price) | ||
| && !diff.is_zero() | ||
| { | ||
| diff.checked_mul_quantity_scaled(&fill.quantity, base_scale) | ||
| .expect("BUG: price_diff * quantity overflow — validated in validate_limit_order") | ||
| } else { | ||
| Quantity::ZERO | ||
| }; | ||
| Self { | ||
| fill, | ||
| notional, | ||
| taker_fee, | ||
| maker_fee, | ||
| surplus, | ||
| } | ||
| } | ||
|
|
||
| /// Push the (up to three) balance operations a single fill settles into `ops`. | ||
| pub fn push_balance_operations(&self, ops: &mut Vec<event::BalanceOperation>) { | ||
| let fill = &self.fill; | ||
| let (buyer_seq, seller_seq) = match fill.taker_side { | ||
| Side::Buy => (fill.taker_order_seq, fill.maker_order_seq), | ||
| Side::Sell => (fill.maker_order_seq, fill.taker_order_seq), | ||
| }; | ||
| let (quote_fee, base_fee) = match fill.taker_side { | ||
| Side::Buy => (self.maker_fee, self.taker_fee), | ||
| Side::Sell => (self.taker_fee, self.maker_fee), | ||
| }; | ||
| ops.push(event::BalanceOperation::Transfer { | ||
| from_order: buyer_seq, | ||
| to_order: seller_seq, | ||
| token: PairToken::Quote, | ||
| amount: self.notional, | ||
| fee: nonzero(quote_fee), | ||
| }); | ||
| if !self.surplus.is_zero() { | ||
| ops.push(event::BalanceOperation::Unreserve { | ||
| order: fill.taker_order_seq, | ||
| token: PairToken::Quote, | ||
| amount: self.surplus, | ||
| }); | ||
| } | ||
| ops.push(event::BalanceOperation::Transfer { | ||
| from_order: seller_seq, | ||
| to_order: buyer_seq, | ||
| token: PairToken::Base, | ||
| amount: fill.quantity, | ||
| fee: nonzero(base_fee), | ||
| }); | ||
| } | ||
|
|
||
| /// Update maker and taker orders based on this fill. | ||
| pub fn accrue_fill(&self, updates: &mut BTreeMap<OrderSeq, OrderUpdate>) { | ||
| for (order_seq, fee) in [ | ||
| (self.fill.maker_order_seq, self.maker_fee), | ||
| (self.fill.taker_order_seq, self.taker_fee), | ||
| ] { | ||
| let update = updates.entry(order_seq).or_default(); | ||
| update.filled_delta = update | ||
| .filled_delta | ||
| .checked_add(self.fill.quantity) | ||
| .expect("BUG: filled_delta overflow"); | ||
| update.quote_delta = update | ||
| .quote_delta | ||
| .checked_add(self.notional) | ||
| .expect("BUG: quote_delta overflow"); | ||
| update.fee_delta = update | ||
| .fee_delta | ||
| .checked_add(fee) | ||
| .expect("BUG: fee_delta overflow"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// The settlement of a removed order (canceled or killed): the placement | ||
| /// reservation released back to its owner, computed where `base_scale` is in | ||
| /// scope so the matcher stays scale-agnostic. | ||
| pub struct RemovedOrderSettlement { | ||
| order_seq: OrderSeq, | ||
| token: PairToken, | ||
| amount: Quantity, | ||
| } | ||
|
|
||
| impl RemovedOrderSettlement { | ||
| /// Compute the reservation released by removing an order. | ||
| pub fn new(order_seq: OrderSeq, removed: &RemovedOrder, base_scale: NonZeroU64) -> Self { | ||
| let (token, amount) = match removed.side { | ||
| Side::Buy => ( | ||
| PairToken::Quote, | ||
| removed | ||
| .price | ||
| .checked_mul_quantity_scaled(&removed.remaining_quantity, base_scale) | ||
| .expect("BUG: price * remaining overflow — validated at placement"), | ||
| ), | ||
| Side::Sell => (PairToken::Base, removed.remaining_quantity), | ||
| }; | ||
| Self { | ||
| order_seq, | ||
| token, | ||
| amount, | ||
| } | ||
| } | ||
|
|
||
| /// Push the single unreserve operation that releases the reservation. | ||
| pub fn push_balance_operations(&self, ops: &mut Vec<event::BalanceOperation>) { | ||
| ops.push(event::BalanceOperation::Unreserve { | ||
| order: self.order_seq, | ||
| token: self.token, | ||
| amount: self.amount, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /// 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 | ||
| /// charged". | ||
| fn nonzero(q: Quantity) -> Option<Quantity> { | ||
| if q.is_zero() { None } else { Some(q) } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.