1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// Copyright The Pit Project Owners. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Please see https://github.com/openpitkit and the OWNERS file for details.
use super::{define_non_negative_value_type, Error, Leverage, ParamKind, Price, Quantity, Volume};
define_non_negative_value_type!(
/// Monetary position exposure used for margin and risk calculation.
///
/// Notional is the absolute monetary value of a position in the settlement
/// currency: `|price| × quantity`. It is always non-negative and represents
/// the full face value of the position regardless of leverage.
///
/// Margin required to hold the position equals `notional / leverage`.
Notional,
ParamKind::Notional
);
impl Notional {
/// Computes notional from price and quantity.
///
/// Uses the absolute value of price so the result is always non-negative,
/// consistent with face-value semantics. Both long and short exposures of
/// the same magnitude produce identical notional.
///
/// # Errors
///
/// Returns [`Error::Overflow`] with [`ParamKind::Price`] when multiplication
/// overflows.
pub fn from_price_quantity(price: Price, quantity: Quantity) -> Result<Self, Error> {
let notional = price
.to_decimal()
.abs()
.checked_mul(quantity.to_decimal())
.ok_or(Error::Overflow {
param: ParamKind::Price,
})?;
Ok(Self::new_unchecked(notional))
}
/// Converts trade volume into position notional.
///
/// Both types represent monetary amounts in the settlement currency; this
/// cast changes the semantic context from "order size" to "position
/// exposure".
pub fn from_volume(volume: Volume) -> Self {
Self::new_unchecked(volume.to_decimal())
}
/// Converts position notional back into settlement volume.
///
/// The inverse of [`Self::from_volume`]; the numeric value is preserved.
pub fn to_volume(self) -> Volume {
Volume::new_unchecked(self.to_decimal())
}
/// Computes the margin required to hold this position at the given leverage.
///
/// Formula: `margin = notional / leverage`.
///
/// Uses exact decimal arithmetic to avoid rounding accumulation.
///
/// # Examples
///
/// ```
/// use openpit::param::{Leverage, Notional};
///
/// let notional = Notional::from_str("10000")?;
/// let leverage = Leverage::from_u16(100)?;
/// let margin = notional.calculate_margin_required(leverage)?;
/// assert_eq!(margin.to_string(), "100");
/// # Ok::<(), openpit::param::Error>(())
/// ```
///
/// # Errors
///
/// Returns [`Error::Overflow`] with [`ParamKind::Notional`] on arithmetic
/// overflow.
pub fn calculate_margin_required(self, leverage: Leverage) -> Result<Self, Error> {
leverage.calculate_margin_required(self)
}
}
#[cfg(test)]
mod tests {
use super::Notional;
use crate::param::{Error, Leverage, ParamKind, Price, Quantity, Volume};
use rust_decimal::Decimal;
fn d(value: &str) -> Decimal {
value
.parse()
.expect("decimal literal in tests must be valid")
}
#[test]
fn from_price_quantity_computes_absolute_notional() {
let price = Price::from_str("42350.75").expect("must be valid");
let qty = Quantity::from_str("0.15").expect("must be valid");
let notional = Notional::from_price_quantity(price, qty).expect("must be valid");
assert_eq!(notional.to_decimal(), d("6352.6125"));
}
#[test]
fn from_price_quantity_uses_absolute_price() {
let price = Price::from_str("-100.0").expect("must be valid");
let qty = Quantity::from_str("2").expect("must be valid");
let notional = Notional::from_price_quantity(price, qty).expect("must be valid");
assert_eq!(notional.to_decimal(), d("200"));
}
#[test]
fn from_price_quantity_reports_overflow() {
let price = Price::new(Decimal::MAX);
let qty = Quantity::from_str("2").expect("must be valid");
assert_eq!(
Notional::from_price_quantity(price, qty),
Err(Error::Overflow {
param: ParamKind::Price
})
);
}
#[test]
fn from_volume_and_to_volume_roundtrip() {
let vol = Volume::from_str("1234.56").expect("must be valid");
let notional = Notional::from_volume(vol);
let back = notional.to_volume();
assert_eq!(back.to_decimal(), vol.to_decimal());
}
#[test]
fn calculate_margin_required_divides_by_leverage() {
let notional = Notional::from_str("10000").expect("must be valid");
let leverage = Leverage::from_u16(100).expect("must be valid");
let margin = notional
.calculate_margin_required(leverage)
.expect("must be valid");
assert_eq!(margin.to_decimal(), d("100"));
}
#[test]
fn calculate_margin_required_with_fractional_leverage() {
let notional = Notional::from_str("1050").expect("must be valid");
let leverage = Leverage::from_f64(10.5).expect("must be valid");
// 1050 * 10 / 105 = 100 exactly
let margin = notional
.calculate_margin_required(leverage)
.expect("must be valid");
assert_eq!(margin.to_decimal(), d("100"));
}
#[test]
fn calculate_margin_required_with_min_leverage() {
let notional = Notional::from_str("5000").expect("must be valid");
let leverage = Leverage::from_u16(1).expect("must be valid");
let margin = notional
.calculate_margin_required(leverage)
.expect("must be valid");
assert_eq!(margin.to_decimal(), d("5000"));
}
#[test]
fn from_price_quantity_zero_quantity_yields_zero() {
let price = Price::from_str("50000").expect("must be valid");
let qty = Quantity::ZERO;
let notional = Notional::from_price_quantity(price, qty).expect("must be valid");
assert!(notional.is_zero());
}
}