#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PivotArtifact {
pub timestamp: i64,
pub price: f64,
pub is_high: bool,
pub confirmed: bool,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ZoneArtifact {
pub kind: String,
pub price_top: f64,
pub price_bottom: f64,
pub strength: f64,
pub touches: u32,
pub from_ts: Option<i64>,
pub to_ts: Option<i64>,
}
impl ZoneArtifact {
pub fn new(kind: impl Into<String>, price_top: f64, price_bottom: f64) -> Self {
Self {
kind: kind.into(),
price_top,
price_bottom,
strength: 0.0,
touches: 0,
from_ts: None,
to_ts: None,
}
}
pub fn spanning(mut self, from_ts: i64, to_ts: i64) -> Self {
self.from_ts = Some(from_ts);
self.to_ts = Some(to_ts);
self
}
pub fn span(&self) -> Option<(i64, i64)> {
self.from_ts.zip(self.to_ts)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ProfileBin {
pub price_low: f64,
pub price_high: f64,
pub value: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ProfileArtifact {
pub kind: String,
pub bins: Vec<ProfileBin>,
pub poc: f64,
pub value_area_high: f64,
pub value_area_low: f64,
pub from_ts: Option<i64>,
pub to_ts: Option<i64>,
}
impl ProfileArtifact {
pub fn spanning(mut self, from_ts: i64, to_ts: i64) -> Self {
self.from_ts = Some(from_ts);
self.to_ts = Some(to_ts);
self
}
pub fn span(&self) -> Option<(i64, i64)> {
self.from_ts.zip(self.to_ts)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ScenarioArtifact {
pub name: String,
pub stage: String,
pub progress: f64,
pub invalidated: bool,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Artifact {
Pivot(PivotArtifact),
Zone(ZoneArtifact),
Profile(ProfileArtifact),
Scenario(ScenarioArtifact),
}
impl From<PivotArtifact> for Artifact {
fn from(value: PivotArtifact) -> Self {
Artifact::Pivot(value)
}
}
impl From<ZoneArtifact> for Artifact {
fn from(value: ZoneArtifact) -> Self {
Artifact::Zone(value)
}
}
impl From<ProfileArtifact> for Artifact {
fn from(value: ProfileArtifact) -> Self {
Artifact::Profile(value)
}
}
impl From<ScenarioArtifact> for Artifact {
fn from(value: ScenarioArtifact) -> Self {
Artifact::Scenario(value)
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TaggedArtifact {
pub artifact: Artifact,
pub series_identity: Option<crate::model::SeriesIdentity>,
}
impl TaggedArtifact {
pub fn new(artifact: Artifact) -> Self {
Self {
artifact,
series_identity: None,
}
}
pub fn with_series(mut self, identity: crate::model::SeriesIdentity) -> Self {
self.series_identity = Some(identity);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::SeriesIdentity;
#[test]
fn test_artifact_conversions() {
let pivot = PivotArtifact {
timestamp: 1_000,
price: 100.0,
is_high: true,
confirmed: true,
};
let artifact: Artifact = pivot.into();
assert!(matches!(artifact, Artifact::Pivot(p) if p.price == 100.0));
let zone = ZoneArtifact {
kind: "order_block".to_string(),
price_top: 105.0,
price_bottom: 100.0,
strength: 0.8,
touches: 2,
from_ts: None,
to_ts: None,
};
let artifact: Artifact = zone.into();
assert!(matches!(&artifact, Artifact::Zone(z) if z.touches == 2));
let series_id = SeriesIdentity::new("ES", "5m");
let tagged = TaggedArtifact::new(artifact).with_series(series_id.clone());
assert_eq!(tagged.series_identity, Some(series_id));
}
#[test]
fn test_zone_span_is_absent_until_set() {
let zone = ZoneArtifact::new("order_block", 105.0, 100.0);
assert_eq!(zone.span(), None, "unknown bounds must not be guessed");
let spanned = zone.spanning(1_000, 2_000);
assert_eq!(spanned.span(), Some((1_000, 2_000)));
}
#[test]
fn test_profile_span_is_absent_until_set() {
let profile = ProfileArtifact {
kind: "volume_profile".to_string(),
bins: Vec::new(),
poc: 100.0,
value_area_high: 101.0,
value_area_low: 99.0,
from_ts: None,
to_ts: None,
};
assert_eq!(profile.span(), None);
assert_eq!(profile.spanning(10, 20).span(), Some((10, 20)));
}
#[test]
fn test_a_half_known_span_reads_as_unknown() {
let mut zone = ZoneArtifact::new("fvg", 105.0, 100.0);
zone.from_ts = Some(1_000);
assert_eq!(zone.span(), None);
}
}