1use std::fmt::Display;
2
3use serde::{Deserialize, Serialize};
4
5define_bytes32_newtype!(OrderId);
6
7define_numeric_newtype!(
8 Strike,
11 u64
12);
13define_numeric_newtype!(
14 Price,
17 u64
18);
19define_numeric_newtype!(
20 Quantity,
22 u64
23);
24
25impl Price {
26 #[must_use]
28 pub fn after_fee_bps(self, bps: u16) -> Self {
29 let fee = u128::from(self.0) * u128::from(bps) / 10_000;
30 match u64::try_from(fee) {
31 Ok(fee) => Self::new(self.0.saturating_sub(fee)),
32 Err(_) => Self::new(0),
33 }
34 }
35}
36
37define_numeric_newtype!(Nonce, u64);
38define_numeric_newtype!(RfqVersion, u64);
39define_numeric_newtype!(OrderVersion, u64);
40
41impl OrderVersion {
42 pub const ACCEPTED: Self = Self::new(1);
44 pub const SUBMITTED: Self = Self::new(2);
46 pub const FAILED: Self = Self::new(3);
48 pub const CONFIRMED: Self = Self::new(4);
50 pub const NOT_FOUND: Self = Self::new(0);
52}
53define_numeric_newtype!(Slot, u64);
54define_numeric_newtype!(ChainId, u64);
55define_numeric_newtype!(DurationSeconds, u64);
56define_numeric_newtype!(Volume, u64);
57define_numeric_newtype!(Balance, u64);
58
59define_numeric_newtype!(QuoteCount, u32);
60define_numeric_newtype!(TradeCount, u32);
61define_numeric_newtype!(TimeoutSeconds, u32);
62
63#[derive(
64 Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
65)]
66pub struct Decimals(pub u8);
67
68impl Decimals {
69 #[must_use]
70 pub const fn new(value: u8) -> Self {
71 Self(value)
72 }
73
74 #[must_use]
75 pub const fn value(self) -> u8 {
76 self.0
77 }
78}
79
80impl Display for Decimals {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 write!(f, "{}", self.0)
83 }
84}
85
86impl From<u8> for Decimals {
87 fn from(value: u8) -> Self {
88 Self(value)
89 }
90}
91
92impl From<Decimals> for u8 {
93 fn from(value: Decimals) -> Self {
94 value.0
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::OrderVersion;
101
102 #[test]
103 fn order_version_ranks_match_wire_lifecycle_precedence() {
104 assert_eq!(OrderVersion::NOT_FOUND.value(), 0);
105 assert!(OrderVersion::ACCEPTED < OrderVersion::SUBMITTED);
106 assert!(OrderVersion::SUBMITTED < OrderVersion::FAILED);
107 assert!(OrderVersion::FAILED < OrderVersion::CONFIRMED);
108 }
109}
110
111macro_rules! define_address_id {
113 ($name:ident) => {
114 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
115 #[serde(from = "crate::Address32", into = "crate::Address32")]
116 pub struct $name {
117 address: crate::Address32,
118 base58: String,
119 }
120 impl $name {
121 #[must_use]
122 pub fn new(address: crate::Address32) -> Self {
123 Self {
124 address,
125 base58: address.to_base58(),
126 }
127 }
128 #[must_use]
129 pub const fn address(&self) -> crate::Address32 {
130 self.address
131 }
132 #[must_use]
133 pub fn as_str(&self) -> &str {
134 &self.base58
135 }
136 }
137 impl From<crate::Address32> for $name {
138 fn from(value: crate::Address32) -> Self {
139 Self::new(value)
140 }
141 }
142 impl From<$name> for crate::Address32 {
143 fn from(value: $name) -> Self {
144 value.address
145 }
146 }
147 impl AsRef<str> for $name {
148 fn as_ref(&self) -> &str {
149 self.as_str()
150 }
151 }
152 impl std::fmt::Display for $name {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.write_str(self.as_str())
155 }
156 }
157 impl std::ops::Deref for $name {
158 type Target = str;
159 fn deref(&self) -> &Self::Target {
160 self.as_str()
161 }
162 }
163 };
164}
165
166define_address_id!(MarketId);
167define_address_id!(UserId);
168
169#[derive(
170 Debug,
171 Clone,
172 Copy,
173 PartialEq,
174 Eq,
175 Hash,
176 Serialize,
177 Deserialize,
178 strum::IntoStaticStr,
179 strum::EnumString,
180)]
181#[serde(rename_all = "snake_case")]
182#[strum(serialize_all = "snake_case")]
183pub enum PositionType {
184 CoveredCall,
185 CashSecuredPut,
186 CallSpread,
187 PutSpread,
188 CashPut,
189}
190
191impl Display for PositionType {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 f.write_str(self.into())
194 }
195}
196
197impl PositionType {
198 pub const fn is_spread(self) -> bool {
199 matches!(self, Self::CallSpread | Self::PutSpread | Self::CashPut)
200 }
201 pub const fn is_put(self) -> bool {
202 matches!(self, Self::CashSecuredPut | Self::PutSpread | Self::CashPut)
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
211#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
212pub enum OptionTerms {
213 CashPut {
215 strike: Strike,
216 },
217 CoveredCall {
218 strike: Strike,
219 },
220 CashSecuredPut {
221 strike: Strike,
222 },
223 CallSpread {
224 lower_strike: Strike,
225 upper_strike: Strike,
226 },
227 PutSpread {
228 lower_strike: Strike,
229 upper_strike: Strike,
230 },
231}
232
233impl OptionTerms {
234 pub const fn is_spread(self) -> bool {
235 self.position_type().is_spread()
236 }
237 #[must_use]
239 pub const fn with_strike(self, strike: Strike) -> Self {
240 match self {
241 Self::CashPut { .. } => Self::CashPut { strike },
242 Self::CoveredCall { .. } => Self::CoveredCall { strike },
243 Self::CashSecuredPut { .. } => Self::CashSecuredPut { strike },
244 Self::CallSpread { upper_strike, .. } => Self::CallSpread {
245 lower_strike: strike,
246 upper_strike,
247 },
248 Self::PutSpread { upper_strike, .. } => Self::PutSpread {
249 lower_strike: strike,
250 upper_strike,
251 },
252 }
253 }
254
255 #[must_use]
256 pub const fn position_type(self) -> PositionType {
257 match self {
258 Self::CashPut { .. } => PositionType::CashPut,
259 Self::CoveredCall { .. } => PositionType::CoveredCall,
260 Self::CashSecuredPut { .. } => PositionType::CashSecuredPut,
261 Self::CallSpread { .. } => PositionType::CallSpread,
262 Self::PutSpread { .. } => PositionType::PutSpread,
263 }
264 }
265
266 #[must_use]
267 pub const fn strike(self) -> Strike {
268 match self {
269 Self::CashPut { strike } => strike,
270 Self::CoveredCall { strike } | Self::CashSecuredPut { strike } => strike,
271 Self::CallSpread { lower_strike, .. } | Self::PutSpread { lower_strike, .. } => {
272 lower_strike
273 }
274 }
275 }
276
277 #[must_use]
278 pub const fn upper_strike(self) -> Option<Strike> {
279 match self {
280 Self::CashPut { strike } => Some(strike),
281 Self::CoveredCall { .. } | Self::CashSecuredPut { .. } => None,
282 Self::CallSpread { upper_strike, .. } | Self::PutSpread { upper_strike, .. } => {
283 Some(upper_strike)
284 }
285 }
286 }
287
288 #[must_use]
290 pub const fn to_chain(self) -> ChainTerms {
291 match self {
292 Self::CoveredCall { strike } => ChainTerms::CoveredCall { strike },
293 Self::CashSecuredPut { strike } => ChainTerms::CashSecuredPut { strike },
294 Self::CallSpread {
295 lower_strike,
296 upper_strike,
297 } => ChainTerms::CallSpread {
298 lower_strike,
299 upper_strike,
300 },
301 Self::PutSpread {
302 lower_strike,
303 upper_strike,
304 } => ChainTerms::PutSpread {
305 lower_strike,
306 upper_strike,
307 },
308 Self::CashPut { strike } => ChainTerms::PutSpread {
309 lower_strike: Strike(0),
310 upper_strike: strike,
311 },
312 }
313 }
314
315 #[must_use]
317 pub const fn from_chain(terms: ChainTerms) -> Self {
318 match terms {
319 ChainTerms::CoveredCall { strike } => Self::CoveredCall { strike },
320 ChainTerms::CashSecuredPut { strike } => Self::CashSecuredPut { strike },
321 ChainTerms::CallSpread {
322 lower_strike,
323 upper_strike,
324 } => Self::CallSpread {
325 lower_strike,
326 upper_strike,
327 },
328 ChainTerms::PutSpread {
329 lower_strike: Strike(0),
330 upper_strike,
331 } => Self::CashPut {
332 strike: upper_strike,
333 },
334 ChainTerms::PutSpread {
335 lower_strike,
336 upper_strike,
337 } => Self::PutSpread {
338 lower_strike,
339 upper_strike,
340 },
341 }
342 }
343
344 pub fn from_chain_parts(
345 product: ChainPositionType,
346 strike: Strike,
347 upper: Option<Strike>,
348 ) -> Result<Self, crate::spreads::SpreadError> {
349 ChainTerms::from_parts(product, strike, upper).map(Self::from_chain)
350 }
351}
352
353#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
355#[serde(rename_all = "snake_case")]
356#[repr(u8)]
357pub enum ChainPositionType {
358 CoveredCall = 0,
359 CashSecuredPut = 1,
360 CallSpread = 2,
361 PutSpread = 3,
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
365#[error("invalid chain position type value: {0}")]
366pub struct ChainPositionTypeParseError(pub u8);
367
368impl TryFrom<u8> for ChainPositionType {
369 type Error = ChainPositionTypeParseError;
370 fn try_from(value: u8) -> Result<Self, Self::Error> {
371 match value {
372 0 => Ok(Self::CoveredCall),
373 1 => Ok(Self::CashSecuredPut),
374 2 => Ok(Self::CallSpread),
375 3 => Ok(Self::PutSpread),
376 _ => Err(ChainPositionTypeParseError(value)),
377 }
378 }
379}
380impl From<ChainPositionType> for u8 {
381 fn from(value: ChainPositionType) -> Self {
382 value as Self
383 }
384}
385impl ChainPositionType {
386 #[must_use]
387 pub const fn is_spread(self) -> bool {
388 matches!(self, Self::CallSpread | Self::PutSpread)
389 }
390 #[must_use]
391 pub const fn is_put(self) -> bool {
392 matches!(self, Self::CashSecuredPut | Self::PutSpread)
393 }
394 #[must_use]
395 pub const fn uses_quote_collateral(self) -> bool {
396 !matches!(self, Self::CoveredCall)
397 }
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401pub enum ChainTerms {
402 CoveredCall {
403 strike: Strike,
404 },
405 CashSecuredPut {
406 strike: Strike,
407 },
408 CallSpread {
409 lower_strike: Strike,
410 upper_strike: Strike,
411 },
412 PutSpread {
413 lower_strike: Strike,
414 upper_strike: Strike,
415 },
416}
417impl ChainTerms {
418 pub fn from_parts(
419 product: ChainPositionType,
420 strike: Strike,
421 upper: Option<Strike>,
422 ) -> Result<Self, crate::spreads::SpreadError> {
423 match (product, upper) {
424 (ChainPositionType::CoveredCall, None) if strike.0 > 0 => {
425 Ok(Self::CoveredCall { strike })
426 }
427 (ChainPositionType::CashSecuredPut, None) if strike.0 > 0 => {
428 Ok(Self::CashSecuredPut { strike })
429 }
430 (ChainPositionType::CallSpread, Some(upper_strike))
431 if strike.0 > 0 && upper_strike > strike =>
432 {
433 Ok(Self::CallSpread {
434 lower_strike: strike,
435 upper_strike,
436 })
437 }
438 (ChainPositionType::PutSpread, Some(upper_strike)) if upper_strike > strike => {
439 Ok(Self::PutSpread {
440 lower_strike: strike,
441 upper_strike,
442 })
443 }
444 _ => Err(crate::spreads::SpreadError::InvalidStrikes),
445 }
446 }
447 #[must_use]
448 pub const fn position_type(self) -> ChainPositionType {
449 match self {
450 Self::CoveredCall { .. } => ChainPositionType::CoveredCall,
451 Self::CashSecuredPut { .. } => ChainPositionType::CashSecuredPut,
452 Self::CallSpread { .. } => ChainPositionType::CallSpread,
453 Self::PutSpread { .. } => ChainPositionType::PutSpread,
454 }
455 }
456 #[must_use]
457 pub const fn strike(self) -> Strike {
458 match self {
459 Self::CoveredCall { strike } | Self::CashSecuredPut { strike } => strike,
460 Self::CallSpread { lower_strike, .. } | Self::PutSpread { lower_strike, .. } => {
461 lower_strike
462 }
463 }
464 }
465 #[must_use]
466 pub const fn upper_strike(self) -> Option<Strike> {
467 match self {
468 Self::CoveredCall { .. } | Self::CashSecuredPut { .. } => None,
469 Self::CallSpread { upper_strike, .. } | Self::PutSpread { upper_strike, .. } => {
470 Some(upper_strike)
471 }
472 }
473 }
474}
475
476#[derive(
482 Debug,
483 Clone,
484 Copy,
485 PartialEq,
486 Eq,
487 Hash,
488 Serialize,
489 Deserialize,
490 strum::IntoStaticStr,
491 strum::EnumString,
492)]
493#[serde(rename_all = "snake_case")]
494#[strum(serialize_all = "snake_case")]
495pub enum TakerSide {
496 Sell,
497 Buy,
498}
499
500impl TakerSide {
501 #[must_use]
502 pub const fn is_taker_buy(self) -> bool {
503 matches!(self, Self::Buy)
504 }
505}
506#[derive(
508 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
509)]
510#[serde(deny_unknown_fields)]
511pub struct PositionRevision {
512 pub slot: u64,
513 pub transaction_index: u32,
514 pub instruction_index: u16,
515}