1use std::fmt;
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14pub struct Currency(String);
15
16impl Currency {
17 pub fn new(code: impl AsRef<str>) -> Self {
19 Self(code.as_ref().trim().to_uppercase())
20 }
21
22 pub fn eur() -> Self {
23 Self("EUR".to_string())
24 }
25
26 pub fn usd() -> Self {
27 Self("USD".to_string())
28 }
29
30 pub fn gbp() -> Self {
31 Self("GBP".to_string())
32 }
33
34 pub fn chf() -> Self {
35 Self("CHF".to_string())
36 }
37
38 pub fn jpy() -> Self {
39 Self("JPY".to_string())
40 }
41
42 pub fn as_str(&self) -> &str {
43 &self.0
44 }
45}
46
47impl fmt::Display for Currency {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.write_str(&self.0)
50 }
51}
52
53impl From<&str> for Currency {
54 fn from(s: &str) -> Self {
55 Self::new(s)
56 }
57}
58
59impl From<String> for Currency {
60 fn from(s: String) -> Self {
61 Self::new(s)
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
68pub enum InstrumentType {
69 Equity,
71 LinearFuture,
73 Forex,
75 Cfd,
77 CryptoSpot,
79 Option,
81}
82
83impl InstrumentType {
84 pub fn is_linear(&self) -> bool {
88 match self {
89 Self::Equity | Self::LinearFuture | Self::Forex | Self::Cfd | Self::CryptoSpot => true,
90 Self::Option => false,
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq)]
97#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
98pub struct ContractSpec {
99 pub price_currency: Currency,
101 pub settlement_currency: Currency,
103 pub multiplier: f64,
105 pub quantity_step: f64,
107 pub min_quantity: f64,
109 pub instrument_type: InstrumentType,
111}
112
113impl Default for ContractSpec {
114 fn default() -> Self {
115 Self {
116 price_currency: Currency::usd(),
117 settlement_currency: Currency::usd(),
118 multiplier: 1.0,
119 quantity_step: 1.0,
120 min_quantity: 1.0,
121 instrument_type: InstrumentType::Equity,
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum ContractSpecError {
129 NonPositiveMultiplier,
130 NonFiniteMultiplier,
131 NonPositiveQuantityStep,
132 NonFiniteQuantityStep,
133 InvalidMinQuantity,
134 EmptyPriceCurrency,
135 EmptySettlementCurrency,
136}
137
138impl fmt::Display for ContractSpecError {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 match self {
141 Self::NonPositiveMultiplier => f.write_str("multiplier must be > 0"),
142 Self::NonFiniteMultiplier => f.write_str("multiplier must be finite"),
143 Self::NonPositiveQuantityStep => f.write_str("quantity_step must be > 0"),
144 Self::NonFiniteQuantityStep => f.write_str("quantity_step must be finite"),
145 Self::InvalidMinQuantity => {
146 f.write_str("min_quantity must be finite and >= quantity_step")
147 }
148 Self::EmptyPriceCurrency => f.write_str("price_currency must not be empty"),
149 Self::EmptySettlementCurrency => f.write_str("settlement_currency must not be empty"),
150 }
151 }
152}
153
154impl std::error::Error for ContractSpecError {}
155
156impl ContractSpec {
157 pub fn validate(&self) -> Result<(), ContractSpecError> {
159 if !self.multiplier.is_finite() {
160 return Err(ContractSpecError::NonFiniteMultiplier);
161 }
162 if self.multiplier <= 0.0 {
163 return Err(ContractSpecError::NonPositiveMultiplier);
164 }
165 if !self.quantity_step.is_finite() {
166 return Err(ContractSpecError::NonFiniteQuantityStep);
167 }
168 if self.quantity_step <= 0.0 {
169 return Err(ContractSpecError::NonPositiveQuantityStep);
170 }
171 if !self.min_quantity.is_finite() || self.min_quantity < self.quantity_step {
172 return Err(ContractSpecError::InvalidMinQuantity);
173 }
174 if self.price_currency.as_str().is_empty() {
175 return Err(ContractSpecError::EmptyPriceCurrency);
176 }
177 if self.settlement_currency.as_str().is_empty() {
178 return Err(ContractSpecError::EmptySettlementCurrency);
179 }
180 Ok(())
181 }
182
183 pub fn round_quantity_down(&self, quantity: f64) -> f64 {
188 if !quantity.is_finite() || quantity <= 0.0 || self.quantity_step <= 0.0 {
189 return 0.0;
190 }
191 let steps = (quantity / self.quantity_step + 1e-12).floor();
192 let rounded = steps * self.quantity_step;
193 if rounded + 1e-12 < self.min_quantity {
195 0.0
196 } else {
197 rounded
198 }
199 }
200}
201
202#[derive(Debug, Clone, PartialEq)]
206#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
207pub struct FxRate {
208 pub base: Currency,
209 pub quote: Currency,
210 pub rate: f64,
211 pub timestamp: i64,
213}
214
215impl FxRate {
216 pub fn new(base: impl Into<Currency>, quote: impl Into<Currency>, rate: f64) -> Self {
217 Self {
218 base: base.into(),
219 quote: quote.into(),
220 rate,
221 timestamp: 0,
222 }
223 }
224
225 pub fn convert(
230 &self,
231 amount: f64,
232 from: &Currency,
233 to: &Currency,
234 ) -> Result<f64, FxConversionError> {
235 if from == to {
236 return Ok(amount);
237 }
238 if !self.rate.is_finite() || self.rate <= 0.0 {
239 return Err(FxConversionError::InvalidRate(self.rate));
240 }
241
242 if from == &self.base && to == &self.quote {
243 Ok(amount * self.rate)
245 } else if from == &self.quote && to == &self.base {
246 Ok(amount / self.rate)
248 } else {
249 Err(FxConversionError::MissingPair {
250 from: from.clone(),
251 to: to.clone(),
252 })
253 }
254 }
255}
256
257#[derive(Debug, Clone, PartialEq)]
259pub enum FxConversionError {
260 InvalidRate(f64),
261 MissingPair { from: Currency, to: Currency },
262 StaleRate { age_seconds: i64, max_age: i64 },
263}
264
265impl fmt::Display for FxConversionError {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 match self {
268 Self::InvalidRate(r) => write!(f, "invalid non-positive FX rate: {r}"),
269 Self::MissingPair { from, to } => {
270 write!(f, "no FX rate available to convert from {from} to {to}")
271 }
272 Self::StaleRate {
273 age_seconds,
274 max_age,
275 } => write!(
276 f,
277 "FX rate is stale: age {age_seconds}s exceeds max {max_age}s"
278 ),
279 }
280 }
281}
282
283impl std::error::Error for FxConversionError {}
284
285#[derive(Debug, Clone, PartialEq)]
287pub enum ValuationError {
288 InvalidContract(ContractSpecError),
289 FxUnavailable(FxConversionError),
290 NonPositivePrice(f64),
291 NonFiniteInput(&'static str),
292}
293
294impl fmt::Display for ValuationError {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 match self {
297 Self::InvalidContract(e) => write!(f, "invalid contract specification: {e}"),
298 Self::FxUnavailable(e) => write!(f, "currency conversion failed: {e}"),
299 Self::NonPositivePrice(p) => write!(f, "price must be positive: {p}"),
300 Self::NonFiniteInput(field) => write!(f, "input {field} must be finite"),
301 }
302 }
303}
304
305impl std::error::Error for ValuationError {}
306
307impl From<ContractSpecError> for ValuationError {
308 fn from(e: ContractSpecError) -> Self {
309 Self::InvalidContract(e)
310 }
311}
312
313impl From<FxConversionError> for ValuationError {
314 fn from(e: FxConversionError) -> Self {
315 Self::FxUnavailable(e)
316 }
317}
318
319pub fn notional_value(
326 price: f64,
327 quantity: f64,
328 spec: &ContractSpec,
329 fx_to_account: Option<f64>,
330) -> Result<f64, ValuationError> {
331 spec.validate()?;
332 if !price.is_finite() || price <= 0.0 {
333 return Err(ValuationError::NonPositivePrice(price));
334 }
335 if !quantity.is_finite() || quantity < 0.0 {
336 return Err(ValuationError::NonFiniteInput("quantity"));
337 }
338 let fx = match fx_to_account {
339 Some(rate) if rate.is_finite() && rate > 0.0 => rate,
340 Some(invalid) => {
341 return Err(ValuationError::FxUnavailable(
342 FxConversionError::InvalidRate(invalid),
343 ))
344 }
345 None => {
346 return Err(ValuationError::FxUnavailable(
347 FxConversionError::MissingPair {
348 from: spec.price_currency.clone(),
349 to: spec.settlement_currency.clone(),
350 },
351 ))
352 }
353 };
354 Ok(quantity * price * spec.multiplier * fx)
355}
356
357pub fn stop_risk_amount(
360 entry: f64,
361 stop: f64,
362 quantity: f64,
363 spec: &ContractSpec,
364 fx_to_account: Option<f64>,
365) -> Result<f64, ValuationError> {
366 spec.validate()?;
367 if !entry.is_finite() || entry <= 0.0 {
368 return Err(ValuationError::NonPositivePrice(entry));
369 }
370 if !stop.is_finite() || stop <= 0.0 {
371 return Err(ValuationError::NonPositivePrice(stop));
372 }
373 if !quantity.is_finite() || quantity < 0.0 {
374 return Err(ValuationError::NonFiniteInput("quantity"));
375 }
376 let fx = match fx_to_account {
377 Some(rate) if rate.is_finite() && rate > 0.0 => rate,
378 Some(invalid) => {
379 return Err(ValuationError::FxUnavailable(
380 FxConversionError::InvalidRate(invalid),
381 ))
382 }
383 None => {
384 return Err(ValuationError::FxUnavailable(
385 FxConversionError::MissingPair {
386 from: spec.price_currency.clone(),
387 to: spec.settlement_currency.clone(),
388 },
389 ))
390 }
391 };
392 let price_diff = (entry - stop).abs();
393 Ok(quantity * price_diff * spec.multiplier * fx)
394}
395
396pub fn contract_pnl(
399 entry: f64,
400 exit: f64,
401 quantity: f64,
402 is_long: bool,
403 spec: &ContractSpec,
404 fx_to_account: Option<f64>,
405) -> Result<f64, ValuationError> {
406 spec.validate()?;
407 if !entry.is_finite() || entry <= 0.0 {
408 return Err(ValuationError::NonPositivePrice(entry));
409 }
410 if !exit.is_finite() || exit <= 0.0 {
411 return Err(ValuationError::NonPositivePrice(exit));
412 }
413 if !quantity.is_finite() || quantity < 0.0 {
414 return Err(ValuationError::NonFiniteInput("quantity"));
415 }
416 let fx = match fx_to_account {
417 Some(rate) if rate.is_finite() && rate > 0.0 => rate,
418 Some(invalid) => {
419 return Err(ValuationError::FxUnavailable(
420 FxConversionError::InvalidRate(invalid),
421 ))
422 }
423 None => {
424 return Err(ValuationError::FxUnavailable(
425 FxConversionError::MissingPair {
426 from: spec.price_currency.clone(),
427 to: spec.settlement_currency.clone(),
428 },
429 ))
430 }
431 };
432 let diff = if is_long { exit - entry } else { entry - exit };
433 Ok(diff * quantity * spec.multiplier * fx)
434}
435
436pub fn contract_tick_value(
439 tick_size: f64,
440 spec: &ContractSpec,
441 fx_to_account: Option<f64>,
442) -> Result<f64, ValuationError> {
443 spec.validate()?;
444 if !tick_size.is_finite() || tick_size <= 0.0 {
445 return Err(ValuationError::NonPositivePrice(tick_size));
446 }
447 let fx = match fx_to_account {
448 Some(rate) if rate.is_finite() && rate > 0.0 => rate,
449 Some(invalid) => {
450 return Err(ValuationError::FxUnavailable(
451 FxConversionError::InvalidRate(invalid),
452 ))
453 }
454 None => {
455 return Err(ValuationError::FxUnavailable(
456 FxConversionError::MissingPair {
457 from: spec.price_currency.clone(),
458 to: spec.settlement_currency.clone(),
459 },
460 ))
461 }
462 };
463 Ok(tick_size * spec.multiplier * fx)
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 #[test]
471 fn test_contract_spec_validation() {
472 let default_spec = ContractSpec::default();
473 assert_eq!(default_spec.validate(), Ok(()));
474
475 let bad_multiplier = ContractSpec {
476 multiplier: 0.0,
477 ..ContractSpec::default()
478 };
479 assert_eq!(
480 bad_multiplier.validate(),
481 Err(ContractSpecError::NonPositiveMultiplier)
482 );
483
484 let bad_step = ContractSpec {
485 quantity_step: -1.0,
486 ..ContractSpec::default()
487 };
488 assert_eq!(
489 bad_step.validate(),
490 Err(ContractSpecError::NonPositiveQuantityStep)
491 );
492
493 let bad_min = ContractSpec {
494 quantity_step: 10.0,
495 min_quantity: 5.0,
496 ..ContractSpec::default()
497 };
498 assert_eq!(
499 bad_min.validate(),
500 Err(ContractSpecError::InvalidMinQuantity)
501 );
502 }
503
504 #[test]
505 fn test_quantity_down_rounding() {
506 let spec = ContractSpec {
507 quantity_step: 0.5,
508 min_quantity: 1.0,
509 ..ContractSpec::default()
510 };
511 assert_eq!(spec.round_quantity_down(2.8), 2.5);
512 assert_eq!(spec.round_quantity_down(1.0), 1.0);
513 assert_eq!(spec.round_quantity_down(0.9), 0.0);
515 assert_eq!(spec.round_quantity_down(0.5), 0.0);
516 }
517
518 #[test]
519 fn test_fx_conversion() {
520 let fx = FxRate::new("EUR", "USD", 1.08);
521
522 assert_eq!(
524 fx.convert(100.0, &Currency::eur(), &Currency::eur())
525 .unwrap(),
526 100.0
527 );
528
529 let usd = fx
531 .convert(100.0, &Currency::eur(), &Currency::usd())
532 .unwrap();
533 assert!((usd - 108.0).abs() < 1e-9);
534
535 let eur = fx
537 .convert(108.0, &Currency::usd(), &Currency::eur())
538 .unwrap();
539 assert!((eur - 100.0).abs() < 1e-9);
540
541 let err = fx.convert(100.0, &Currency::gbp(), &Currency::usd());
543 assert!(matches!(err, Err(FxConversionError::MissingPair { .. })));
544 }
545}