1use std::fmt;
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8#[cfg_attr(
9 feature = "serde",
10 derive(Serialize, Deserialize),
11 serde(rename_all = "lowercase")
12)]
13pub enum Resolution {
14 M1,
15 M5,
16 M15,
17 M30,
18 H1,
19 H4,
20 D1,
21 W1,
22}
23
24impl Resolution {
25 pub fn as_str(&self) -> &'static str {
26 match self {
27 Resolution::M1 => "1m",
28 Resolution::M5 => "5m",
29 Resolution::M15 => "15m",
30 Resolution::M30 => "30m",
31 Resolution::H1 => "1h",
32 Resolution::H4 => "4h",
33 Resolution::D1 => "1d",
34 Resolution::W1 => "1w",
35 }
36 }
37}
38
39impl fmt::Display for Resolution {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 write!(f, "{}", self.as_str())
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
47#[cfg_attr(
48 feature = "serde",
49 derive(Serialize, Deserialize),
50 serde(rename_all = "lowercase")
51)]
52pub enum Source {
53 #[default]
54 Close,
55 Open,
56 High,
57 Low,
58 Hl2,
59 Hlc3,
60 Ohlc4,
61 Volume,
62 TypicalPrice,
63}
64
65impl Source {
66 pub fn extract(&self, bar: &Bar) -> f64 {
68 match self {
69 Source::Close => bar.close,
70 Source::Open => bar.open,
71 Source::High => bar.high,
72 Source::Low => bar.low,
73 Source::Hl2 => (bar.high + bar.low) / 2.0,
74 Source::Hlc3 => (bar.high + bar.low + bar.close) / 3.0,
75 Source::Ohlc4 => (bar.open + bar.high + bar.low + bar.close) / 4.0,
76 Source::Volume => bar.volume,
77 Source::TypicalPrice => bar.typical_price(),
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq)]
84#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
85pub struct InstrumentMeta {
86 pub symbol: String,
87 pub tick_size: f64,
88 pub price_precision: usize,
89 pub timezone: String,
90}
91
92impl Default for InstrumentMeta {
93 fn default() -> Self {
94 Self {
95 symbol: "GENERIC".to_string(),
96 tick_size: 0.01,
97 price_precision: 2,
98 timezone: "UTC".to_string(),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum InstrumentMetaError {
106 NonPositiveTickSize,
107 NonFiniteTickSize,
108 ExcessivePricePrecision,
109 EmptySymbol,
110 EmptyTimezone,
111}
112
113impl fmt::Display for InstrumentMetaError {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 let message = match self {
116 Self::NonPositiveTickSize => "tick_size must be greater than zero",
117 Self::NonFiniteTickSize => "tick_size must be finite",
118 Self::ExcessivePricePrecision => "price_precision must be <= 12",
119 Self::EmptySymbol => "symbol must not be empty",
120 Self::EmptyTimezone => "timezone must not be empty",
121 };
122 f.write_str(message)
123 }
124}
125
126impl std::error::Error for InstrumentMetaError {}
127
128impl InstrumentMeta {
129 pub fn validate(&self) -> Result<(), InstrumentMetaError> {
133 if !self.tick_size.is_finite() {
134 return Err(InstrumentMetaError::NonFiniteTickSize);
135 }
136 if self.tick_size <= 0.0 {
137 return Err(InstrumentMetaError::NonPositiveTickSize);
138 }
139 if self.price_precision > 12 {
140 return Err(InstrumentMetaError::ExcessivePricePrecision);
141 }
142 if self.symbol.trim().is_empty() {
143 return Err(InstrumentMetaError::EmptySymbol);
144 }
145 if self.timezone.trim().is_empty() {
146 return Err(InstrumentMetaError::EmptyTimezone);
147 }
148 Ok(())
149 }
150
151 pub fn round_to_tick(&self, price: f64) -> f64 {
157 if !self.tick_size.is_finite() || self.tick_size <= 0.0 || !price.is_finite() {
158 return price;
159 }
160 (price / self.tick_size).round() * self.tick_size
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
167#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
168pub struct BarQuality {
169 pub volume_available: bool,
172 pub is_synthetic: bool,
175 pub is_forward_filled: bool,
177 pub has_gap: bool,
179}
180
181impl BarQuality {
182 pub fn observed() -> Self {
185 Self {
186 volume_available: true,
187 is_synthetic: false,
188 is_forward_filled: false,
189 has_gap: false,
190 }
191 }
192}
193
194#[derive(Debug, Clone, PartialEq)]
196#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
197pub struct QualifiedBar {
198 pub bar: Bar,
199 pub quality: BarQuality,
200}
201
202impl QualifiedBar {
203 pub fn new(bar: Bar, quality: BarQuality) -> Self {
204 Self { bar, quality }
205 }
206
207 pub fn observed(bar: Bar) -> Self {
209 Self {
210 bar,
211 quality: BarQuality::observed(),
212 }
213 }
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224#[cfg_attr(
225 feature = "serde",
226 derive(Serialize, Deserialize),
227 serde(rename_all = "snake_case")
228)]
229pub enum VolumeKind {
230 None,
232 Tick,
234 RealTurnover,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244#[cfg_attr(
245 feature = "serde",
246 derive(Serialize, Deserialize),
247 serde(rename_all = "snake_case")
248)]
249pub enum SessionKind {
250 Regular,
252 Extended,
254 Continuous,
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265#[cfg_attr(
266 feature = "serde",
267 derive(Serialize, Deserialize),
268 serde(rename_all = "snake_case")
269)]
270pub enum ContinuityKind {
271 Spot,
273 SingleContract,
275 StitchedUnadjusted,
277 StitchedBackAdjusted,
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288#[cfg_attr(
289 feature = "serde",
290 derive(Serialize, Deserialize),
291 serde(rename_all = "snake_case")
292)]
293pub enum PriceAdjustment {
294 Raw,
296 Split,
298 SplitAndDividend,
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309#[cfg_attr(
310 feature = "serde",
311 derive(Serialize, Deserialize),
312 serde(rename_all = "snake_case")
313)]
314pub enum Provenance {
315 Exchange,
317 Broker,
319 Synthetic,
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328#[cfg_attr(
329 feature = "serde",
330 derive(Serialize, Deserialize),
331 serde(rename_all = "snake_case")
332)]
333pub enum LiquidityTier {
334 Deep,
335 Normal,
336 Thin,
337 Unknown,
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
351pub struct SeriesCapabilities {
352 pub volume: VolumeKind,
353 pub trade_direction: bool,
356 pub session: SessionKind,
357 pub continuity: ContinuityKind,
358 pub price_adjustment: PriceAdjustment,
359 pub provenance: Provenance,
360 pub liquidity_tier: LiquidityTier,
361}
362
363#[derive(Debug, Clone, PartialEq, Eq)]
369#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
370pub struct SeriesIdentity {
371 pub symbol: String,
372 pub timeframe: String,
373 pub session: SessionKind,
374 pub continuity: ContinuityKind,
375 pub price_adjustment: PriceAdjustment,
376 pub provenance: Provenance,
377 pub contract_code: Option<String>,
378}
379
380impl SeriesIdentity {
381 pub fn new(symbol: impl Into<String>, timeframe: impl Into<String>) -> Self {
382 Self {
383 symbol: symbol.into(),
384 timeframe: timeframe.into(),
385 session: SessionKind::Regular,
386 continuity: ContinuityKind::Spot,
387 price_adjustment: PriceAdjustment::Raw,
388 provenance: Provenance::Exchange,
389 contract_code: None,
390 }
391 }
392
393 pub fn with_session(mut self, session: SessionKind) -> Self {
394 self.session = session;
395 self
396 }
397
398 pub fn with_continuity(mut self, continuity: ContinuityKind) -> Self {
399 self.continuity = continuity;
400 self
401 }
402
403 pub fn with_price_adjustment(mut self, adj: PriceAdjustment) -> Self {
404 self.price_adjustment = adj;
405 self
406 }
407
408 pub fn with_provenance(mut self, prov: Provenance) -> Self {
409 self.provenance = prov;
410 self
411 }
412
413 pub fn with_contract_code(mut self, code: impl Into<String>) -> Self {
414 self.contract_code = Some(code.into());
415 self
416 }
417
418 pub fn is_compatible(&self, other: &Self) -> bool {
421 self.symbol == other.symbol
422 && self.session == other.session
423 && self.continuity == other.continuity
424 && self.price_adjustment == other.price_adjustment
425 && self.contract_code == other.contract_code
426 }
427}
428
429#[derive(Debug, Clone, PartialEq)]
431#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
432pub struct Bar {
433 pub timestamp: i64,
434 pub open: f64,
435 pub high: f64,
436 pub low: f64,
437 pub close: f64,
438 pub volume: f64,
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
443pub enum BarValidationError {
444 NonFiniteValue,
445 NonPositivePrice,
446 NegativeVolume,
447 InvalidPriceRange,
448}
449
450impl fmt::Display for BarValidationError {
451 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452 let message = match self {
453 Self::NonFiniteValue => "OHLCV values must be finite",
454 Self::NonPositivePrice => "OHLC prices must be greater than zero",
455 Self::NegativeVolume => "volume must be non-negative",
456 Self::InvalidPriceRange => "low/high must contain open and close",
457 };
458 f.write_str(message)
459 }
460}
461
462impl std::error::Error for BarValidationError {}
463
464impl Bar {
465 pub fn new(timestamp: i64, open: f64, high: f64, low: f64, close: f64, volume: f64) -> Self {
470 Self {
471 timestamp,
472 open,
473 high,
474 low,
475 close,
476 volume,
477 }
478 }
479
480 pub fn try_new(
482 timestamp: i64,
483 open: f64,
484 high: f64,
485 low: f64,
486 close: f64,
487 volume: f64,
488 ) -> Result<Self, BarValidationError> {
489 let bar = Self::new(timestamp, open, high, low, close, volume);
490 bar.validate()?;
491 Ok(bar)
492 }
493
494 pub fn typical_price(&self) -> f64 {
495 (self.high + self.low + self.close) / 3.0
496 }
497
498 pub fn validate(&self) -> Result<(), BarValidationError> {
503 if !self.open.is_finite()
504 || !self.high.is_finite()
505 || !self.low.is_finite()
506 || !self.close.is_finite()
507 || !self.volume.is_finite()
508 {
509 return Err(BarValidationError::NonFiniteValue);
510 }
511
512 if self.open <= 0.0 || self.high <= 0.0 || self.low <= 0.0 || self.close <= 0.0 {
513 return Err(BarValidationError::NonPositivePrice);
514 }
515 if self.volume < 0.0 {
516 return Err(BarValidationError::NegativeVolume);
517 }
518
519 let min_oc = self.open.min(self.close);
520 let max_oc = self.open.max(self.close);
521
522 if self.low > min_oc || self.high < max_oc || self.low > self.high {
523 return Err(BarValidationError::InvalidPriceRange);
524 }
525
526 Ok(())
527 }
528
529 pub fn is_valid(&self) -> bool {
531 self.validate().is_ok()
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538
539 #[test]
540 fn test_bar_validation_contract() {
541 let valid = Bar::new(1000, 100.0, 105.0, 95.0, 104.0, 1000.0);
542 assert!(valid.is_valid());
543 assert_eq!(
544 Bar::try_new(1000, 100.0, 105.0, 95.0, 104.0, 1000.0),
545 Ok(valid)
546 );
547
548 let neg_price = Bar::new(1000, -100.0, 105.0, 95.0, 104.0, 1000.0);
550 assert!(!neg_price.is_valid());
551
552 let neg_vol = Bar::new(1000, 100.0, 105.0, 95.0, 104.0, -10.0);
554 assert!(!neg_vol.is_valid());
555
556 let bad_high = Bar::new(1000, 100.0, 90.0, 80.0, 95.0, 1000.0);
558 assert!(!bad_high.is_valid());
559
560 let nan_price = Bar::new(1000, f64::NAN, 105.0, 95.0, 104.0, 1000.0);
562 assert!(!nan_price.is_valid());
563 assert_eq!(
564 nan_price.validate(),
565 Err(BarValidationError::NonFiniteValue)
566 );
567 }
568
569 #[test]
570 fn test_instrument_meta_validate() {
571 assert_eq!(InstrumentMeta::default().validate(), Ok(()));
572
573 let bad_tick = InstrumentMeta {
574 tick_size: 0.0,
575 ..InstrumentMeta::default()
576 };
577 assert_eq!(
578 bad_tick.validate(),
579 Err(InstrumentMetaError::NonPositiveTickSize)
580 );
581
582 let bad_precision = InstrumentMeta {
583 price_precision: 13,
584 ..InstrumentMeta::default()
585 };
586 assert_eq!(
587 bad_precision.validate(),
588 Err(InstrumentMetaError::ExcessivePricePrecision)
589 );
590
591 let empty_symbol = InstrumentMeta {
592 symbol: "".to_string(),
593 ..InstrumentMeta::default()
594 };
595 assert_eq!(
596 empty_symbol.validate(),
597 Err(InstrumentMetaError::EmptySymbol)
598 );
599 }
600
601 #[test]
602 fn test_instrument_meta_round_to_tick() {
603 let meta = InstrumentMeta {
604 tick_size: 0.25,
605 ..InstrumentMeta::default()
606 };
607 assert_eq!(meta.round_to_tick(100.10), 100.0);
608 assert_eq!(meta.round_to_tick(100.13), 100.25);
609 assert_eq!(meta.round_to_tick(100.125), 100.25);
610
611 let invalid_tick = InstrumentMeta {
612 tick_size: 0.0,
613 ..InstrumentMeta::default()
614 };
615 assert_eq!(invalid_tick.round_to_tick(100.10), 100.10);
617 }
618
619 #[test]
620 fn test_bar_quality_defaults() {
621 let default_quality = BarQuality::default();
622 assert!(!default_quality.volume_available);
623 assert!(!default_quality.is_synthetic);
624
625 let observed = BarQuality::observed();
626 assert!(observed.volume_available);
627 assert!(!observed.is_synthetic);
628 assert!(!observed.is_forward_filled);
629 assert!(!observed.has_gap);
630
631 let bar = Bar::new(1000, 100.0, 105.0, 95.0, 104.0, 1000.0);
632 let qualified = QualifiedBar::observed(bar.clone());
633 assert_eq!(qualified.bar, bar);
634 assert_eq!(qualified.quality, BarQuality::observed());
635 }
636}
637
638#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
640#[cfg_attr(
641 feature = "serde",
642 derive(Serialize, Deserialize),
643 serde(rename_all = "snake_case")
644)]
645pub enum MarketRegime {
646 BullishExpansion,
647 BearishExpansion,
648 #[default]
649 Consolidation,
650 Transition,
651}
652
653impl fmt::Display for MarketRegime {
654 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655 match self {
656 MarketRegime::BullishExpansion => write!(f, "Bullish Expansion"),
657 MarketRegime::BearishExpansion => write!(f, "Bearish Expansion"),
658 MarketRegime::Consolidation => write!(f, "Consolidation / Range"),
659 MarketRegime::Transition => write!(f, "Regime Transition"),
660 }
661 }
662}
663
664#[derive(Debug, Clone, Copy, PartialEq, Eq)]
665#[cfg_attr(
666 feature = "serde",
667 derive(Serialize, Deserialize),
668 serde(rename_all = "snake_case")
669)]
670pub enum ZoneKind {
671 Support,
672 Resistance,
673}
674
675#[derive(Debug, Clone, PartialEq)]
676#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
677pub struct SupportResistanceZone {
678 pub kind: ZoneKind,
679 pub price: f64,
680 pub price_top: f64,
681 pub price_bottom: f64,
682 pub strength: f64, pub distance_pct: f64,
684 pub touches: u32,
685}
686
687#[derive(Debug, Clone, PartialEq)]
688#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
689pub struct RiskPlan {
690 pub entry: f64,
691 pub stop_loss: f64,
692 pub target_1: f64,
693 pub target_2: f64,
694 pub risk_reward_ratio: f64,
695}
696
697impl RiskPlan {
698 pub fn rounded_to(&self, instrument: &InstrumentMeta) -> Self {
702 let entry = instrument.round_to_tick(self.entry);
703 let stop_loss = instrument.round_to_tick(self.stop_loss);
704 let target_1 = instrument.round_to_tick(self.target_1);
705 let target_2 = instrument.round_to_tick(self.target_2);
706
707 let risk = (entry - stop_loss).abs();
708 let reward = (target_2 - entry).abs();
709 let risk_reward_ratio = if risk > 0.0 {
710 reward / risk
711 } else {
712 self.risk_reward_ratio
713 };
714
715 Self {
716 entry,
717 stop_loss,
718 target_1,
719 target_2,
720 risk_reward_ratio,
721 }
722 }
723}
724
725#[cfg(test)]
726mod risk_plan_tests {
727 use super::*;
728
729 #[test]
730 fn test_risk_plan_rounded_to_tick() {
731 let plan = RiskPlan {
732 entry: 100.13,
733 stop_loss: 98.77,
734 target_1: 101.5,
735 target_2: 103.02,
736 risk_reward_ratio: 2.11,
737 };
738 let instrument = InstrumentMeta {
739 tick_size: 0.25,
740 ..InstrumentMeta::default()
741 };
742 let rounded = plan.rounded_to(&instrument);
743
744 assert_eq!(rounded.entry, 100.25);
745 assert_eq!(rounded.stop_loss, 98.75);
746 assert_eq!(rounded.target_1, 101.5);
747 assert_eq!(rounded.target_2, 103.0);
748
749 let expected_rrr = (103.0f64 - 100.25).abs() / (100.25f64 - 98.75).abs();
750 assert!((rounded.risk_reward_ratio - expected_rrr).abs() < 1e-9);
751 }
752}