1#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15pub struct PivotArtifact {
16 pub timestamp: i64,
17 pub price: f64,
18 pub is_high: bool,
19 pub confirmed: bool,
20}
21
22#[derive(Debug, Clone, PartialEq)]
29#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
30pub struct ZoneArtifact {
31 pub kind: String,
32 pub price_top: f64,
33 pub price_bottom: f64,
34 pub strength: f64,
35 pub touches: u32,
36 pub from_ts: Option<i64>,
38 pub to_ts: Option<i64>,
40}
41
42impl ZoneArtifact {
43 pub fn new(kind: impl Into<String>, price_top: f64, price_bottom: f64) -> Self {
45 Self {
46 kind: kind.into(),
47 price_top,
48 price_bottom,
49 strength: 0.0,
50 touches: 0,
51 from_ts: None,
52 to_ts: None,
53 }
54 }
55
56 pub fn spanning(mut self, from_ts: i64, to_ts: i64) -> Self {
58 self.from_ts = Some(from_ts);
59 self.to_ts = Some(to_ts);
60 self
61 }
62
63 pub fn span(&self) -> Option<(i64, i64)> {
65 self.from_ts.zip(self.to_ts)
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq)]
71#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
72pub struct ProfileBin {
73 pub price_low: f64,
74 pub price_high: f64,
75 pub value: f64,
76}
77
78#[derive(Debug, Clone, PartialEq)]
83#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
84pub struct ProfileArtifact {
85 pub kind: String,
86 pub bins: Vec<ProfileBin>,
87 pub poc: f64,
88 pub value_area_high: f64,
89 pub value_area_low: f64,
90 pub from_ts: Option<i64>,
92 pub to_ts: Option<i64>,
94}
95
96impl ProfileArtifact {
97 pub fn spanning(mut self, from_ts: i64, to_ts: i64) -> Self {
99 self.from_ts = Some(from_ts);
100 self.to_ts = Some(to_ts);
101 self
102 }
103
104 pub fn span(&self) -> Option<(i64, i64)> {
106 self.from_ts.zip(self.to_ts)
107 }
108}
109
110#[derive(Debug, Clone, PartialEq)]
112#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
113pub struct ScenarioArtifact {
114 pub name: String,
115 pub stage: String,
116 pub progress: f64,
117 pub invalidated: bool,
118}
119
120#[derive(Debug, Clone, PartialEq)]
122#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
123pub enum Artifact {
124 Pivot(PivotArtifact),
125 Zone(ZoneArtifact),
126 Profile(ProfileArtifact),
127 Scenario(ScenarioArtifact),
128}
129
130impl From<PivotArtifact> for Artifact {
131 fn from(value: PivotArtifact) -> Self {
132 Artifact::Pivot(value)
133 }
134}
135
136impl From<ZoneArtifact> for Artifact {
137 fn from(value: ZoneArtifact) -> Self {
138 Artifact::Zone(value)
139 }
140}
141
142impl From<ProfileArtifact> for Artifact {
143 fn from(value: ProfileArtifact) -> Self {
144 Artifact::Profile(value)
145 }
146}
147
148impl From<ScenarioArtifact> for Artifact {
149 fn from(value: ScenarioArtifact) -> Self {
150 Artifact::Scenario(value)
151 }
152}
153
154#[derive(Debug, Clone, PartialEq)]
159#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
160pub struct TaggedArtifact {
161 pub artifact: Artifact,
162 pub series_identity: Option<crate::model::SeriesIdentity>,
163}
164
165impl TaggedArtifact {
166 pub fn new(artifact: Artifact) -> Self {
167 Self {
168 artifact,
169 series_identity: None,
170 }
171 }
172
173 pub fn with_series(mut self, identity: crate::model::SeriesIdentity) -> Self {
174 self.series_identity = Some(identity);
175 self
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::model::SeriesIdentity;
183
184 #[test]
185 fn test_artifact_conversions() {
186 let pivot = PivotArtifact {
187 timestamp: 1_000,
188 price: 100.0,
189 is_high: true,
190 confirmed: true,
191 };
192 let artifact: Artifact = pivot.into();
193 assert!(matches!(artifact, Artifact::Pivot(p) if p.price == 100.0));
194
195 let zone = ZoneArtifact {
196 kind: "order_block".to_string(),
197 price_top: 105.0,
198 price_bottom: 100.0,
199 strength: 0.8,
200 touches: 2,
201 from_ts: None,
202 to_ts: None,
203 };
204 let artifact: Artifact = zone.into();
205 assert!(matches!(&artifact, Artifact::Zone(z) if z.touches == 2));
206
207 let series_id = SeriesIdentity::new("ES", "5m");
208 let tagged = TaggedArtifact::new(artifact).with_series(series_id.clone());
209 assert_eq!(tagged.series_identity, Some(series_id));
210 }
211
212 #[test]
213 fn test_zone_span_is_absent_until_set() {
214 let zone = ZoneArtifact::new("order_block", 105.0, 100.0);
215 assert_eq!(zone.span(), None, "unknown bounds must not be guessed");
216
217 let spanned = zone.spanning(1_000, 2_000);
218 assert_eq!(spanned.span(), Some((1_000, 2_000)));
219 }
220
221 #[test]
222 fn test_profile_span_is_absent_until_set() {
223 let profile = ProfileArtifact {
224 kind: "volume_profile".to_string(),
225 bins: Vec::new(),
226 poc: 100.0,
227 value_area_high: 101.0,
228 value_area_low: 99.0,
229 from_ts: None,
230 to_ts: None,
231 };
232 assert_eq!(profile.span(), None);
233 assert_eq!(profile.spanning(10, 20).span(), Some((10, 20)));
234 }
235
236 #[test]
239 fn test_a_half_known_span_reads_as_unknown() {
240 let mut zone = ZoneArtifact::new("fvg", 105.0, 100.0);
241 zone.from_ts = Some(1_000);
242 assert_eq!(zone.span(), None);
243 }
244}