fin_primitives/types/
mod.rs1use crate::error::FinError;
19use chrono::{DateTime, TimeZone, Utc};
20use rust_decimal::Decimal;
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
31pub struct Symbol(String);
32
33impl Symbol {
34 pub fn new(s: impl Into<String>) -> Result<Self, FinError> {
39 let s = s.into();
40 if s.is_empty() || s.chars().any(char::is_whitespace) {
41 return Err(FinError::InvalidSymbol(s));
42 }
43 Ok(Self(s))
44 }
45
46 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50}
51
52impl std::fmt::Display for Symbol {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.write_str(&self.0)
55 }
56}
57
58#[derive(
68 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
69)]
70pub struct Price(Decimal);
71
72impl Price {
73 pub fn new(d: Decimal) -> Result<Self, FinError> {
78 if d <= Decimal::ZERO {
79 return Err(FinError::InvalidPrice(d));
80 }
81 Ok(Self(d))
82 }
83
84 pub fn value(&self) -> Decimal {
86 self.0
87 }
88}
89
90#[derive(
100 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
101)]
102pub struct Quantity(Decimal);
103
104impl Quantity {
105 pub fn new(d: Decimal) -> Result<Self, FinError> {
110 if d < Decimal::ZERO {
111 return Err(FinError::InvalidQuantity(d));
112 }
113 Ok(Self(d))
114 }
115
116 pub fn zero() -> Self {
118 Self(Decimal::ZERO)
119 }
120
121 pub fn value(&self) -> Decimal {
123 self.0
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
129pub enum Side {
130 Bid,
132 Ask,
134}
135
136#[derive(
140 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
141)]
142pub struct NanoTimestamp(pub i64);
143
144impl NanoTimestamp {
145 pub fn now() -> Self {
149 Self(Utc::now().timestamp_nanos_opt().unwrap_or(0))
150 }
151
152 pub fn to_datetime(&self) -> DateTime<Utc> {
154 let secs = self.0 / 1_000_000_000;
155 #[allow(clippy::cast_sign_loss)]
156 let nanos = (self.0 % 1_000_000_000) as u32;
157 Utc.timestamp_opt(secs, nanos).single().unwrap_or_else(|| {
158 Utc.timestamp_opt(0, 0)
159 .single()
160 .unwrap_or(DateTime::<Utc>::MIN_UTC)
161 })
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use rust_decimal_macros::dec;
169
170 #[test]
173 fn test_symbol_new_valid_ok() {
174 let sym = Symbol::new("AAPL").unwrap();
175 assert_eq!(sym.as_str(), "AAPL");
176 }
177
178 #[test]
179 fn test_symbol_new_empty_fails() {
180 let result = Symbol::new("");
181 assert!(matches!(result, Err(FinError::InvalidSymbol(_))));
182 }
183
184 #[test]
185 fn test_symbol_new_whitespace_fails() {
186 let result = Symbol::new("AA PL");
187 assert!(matches!(result, Err(FinError::InvalidSymbol(_))));
188 }
189
190 #[test]
191 fn test_symbol_new_leading_whitespace_fails() {
192 let result = Symbol::new(" AAPL");
193 assert!(matches!(result, Err(FinError::InvalidSymbol(_))));
194 }
195
196 #[test]
197 fn test_symbol_display() {
198 let sym = Symbol::new("TSLA").unwrap();
199 assert_eq!(format!("{sym}"), "TSLA");
200 }
201
202 #[test]
203 fn test_symbol_clone_equality() {
204 let a = Symbol::new("BTC").unwrap();
205 let b = a.clone();
206 assert_eq!(a, b);
207 }
208
209 #[test]
212 fn test_price_new_positive_ok() {
213 let p = Price::new(dec!(100.5)).unwrap();
214 assert_eq!(p.value(), dec!(100.5));
215 }
216
217 #[test]
218 fn test_price_new_zero_fails() {
219 let result = Price::new(dec!(0));
220 assert!(matches!(result, Err(FinError::InvalidPrice(_))));
221 }
222
223 #[test]
224 fn test_price_new_negative_fails() {
225 let result = Price::new(dec!(-1));
226 assert!(matches!(result, Err(FinError::InvalidPrice(_))));
227 }
228
229 #[test]
230 fn test_price_ordering() {
231 let p1 = Price::new(dec!(1)).unwrap();
232 let p2 = Price::new(dec!(2)).unwrap();
233 assert!(p1 < p2);
234 }
235
236 #[test]
239 fn test_quantity_new_zero_ok() {
240 let q = Quantity::new(dec!(0)).unwrap();
241 assert_eq!(q.value(), dec!(0));
242 }
243
244 #[test]
245 fn test_quantity_new_positive_ok() {
246 let q = Quantity::new(dec!(5.5)).unwrap();
247 assert_eq!(q.value(), dec!(5.5));
248 }
249
250 #[test]
251 fn test_quantity_new_negative_fails() {
252 let result = Quantity::new(dec!(-0.01));
253 assert!(matches!(result, Err(FinError::InvalidQuantity(_))));
254 }
255
256 #[test]
257 fn test_quantity_zero_constructor() {
258 let q = Quantity::zero();
259 assert_eq!(q.value(), Decimal::ZERO);
260 }
261
262 #[test]
265 fn test_nano_timestamp_now_positive() {
266 let ts = NanoTimestamp::now();
267 assert!(ts.0 > 0);
268 }
269
270 #[test]
271 fn test_nano_timestamp_ordering() {
272 let ts1 = NanoTimestamp(1_000_000_000);
273 let ts2 = NanoTimestamp(2_000_000_000);
274 assert!(ts1 < ts2);
275 }
276
277 #[test]
278 fn test_nano_timestamp_to_datetime_epoch() {
279 let ts = NanoTimestamp(0);
280 let dt = ts.to_datetime();
281 assert_eq!(dt.timestamp(), 0);
282 }
283
284 #[test]
285 fn test_nano_timestamp_to_datetime_roundtrip() {
286 let ts = NanoTimestamp(1_700_000_000_000_000_000_i64);
287 let dt = ts.to_datetime();
288 assert_eq!(
289 dt.timestamp_nanos_opt().unwrap_or(0),
290 1_700_000_000_000_000_000_i64
291 );
292 }
293}