#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScenarioStatus {
Active,
Expired,
Invalidated,
Completed,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StageConfig<S> {
pub stage: S,
pub max_bars: Option<u32>,
}
pub struct ScenarioStateMachine<S: Copy + PartialEq> {
stages: Vec<StageConfig<S>>,
terminal_stages: Vec<S>,
current: S,
bars_in_stage: u32,
status: ScenarioStatus,
history: Vec<(S, i64)>,
}
impl<S: Copy + PartialEq> ScenarioStateMachine<S> {
pub fn new(
stages: Vec<StageConfig<S>>,
terminal_stages: Vec<S>,
initial: S,
started_at: i64,
) -> Self {
Self {
stages,
terminal_stages,
current: initial,
bars_in_stage: 0,
status: ScenarioStatus::Active,
history: vec![(initial, started_at)],
}
}
pub fn status(&self) -> ScenarioStatus {
self.status
}
pub fn current_stage(&self) -> S {
self.current
}
pub fn history(&self) -> &[(S, i64)] {
&self.history
}
pub fn bars_in_stage(&self) -> u32 {
self.bars_in_stage
}
pub fn tick(&mut self) {
if self.status != ScenarioStatus::Active {
return;
}
self.bars_in_stage += 1;
if let Some(cfg) = self.stages.iter().find(|c| c.stage == self.current) {
if let Some(max) = cfg.max_bars {
if self.bars_in_stage > max {
self.status = ScenarioStatus::Expired;
}
}
}
}
pub fn advance(&mut self, next_stage: S, timestamp: i64) -> bool {
if self.status != ScenarioStatus::Active {
return false;
}
self.current = next_stage;
self.bars_in_stage = 0;
self.history.push((next_stage, timestamp));
if self.terminal_stages.contains(&next_stage) {
self.status = ScenarioStatus::Completed;
}
true
}
pub fn invalidate(&mut self) {
if self.status == ScenarioStatus::Active {
self.status = ScenarioStatus::Invalidated;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EdgeSetupStage {
Edge,
Setup,
Watch,
Trigger,
}
pub fn edge_setup_watch_trigger_machine(
max_bars_per_stage: u32,
started_at: i64,
) -> ScenarioStateMachine<EdgeSetupStage> {
let stages = [
EdgeSetupStage::Edge,
EdgeSetupStage::Setup,
EdgeSetupStage::Watch,
EdgeSetupStage::Trigger,
]
.into_iter()
.map(|stage| StageConfig {
stage,
max_bars: Some(max_bars_per_stage),
})
.collect();
ScenarioStateMachine::new(
stages,
vec![EdgeSetupStage::Trigger],
EdgeSetupStage::Edge,
started_at,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BalanceBreakoutStage {
ArmedBalance,
Breakout,
Aftermath,
}
pub fn armed_balance_breakout_aftermath_machine(
armed_max_bars: u32,
breakout_max_bars: u32,
started_at: i64,
) -> ScenarioStateMachine<BalanceBreakoutStage> {
let stages = vec![
StageConfig {
stage: BalanceBreakoutStage::ArmedBalance,
max_bars: Some(armed_max_bars),
},
StageConfig {
stage: BalanceBreakoutStage::Breakout,
max_bars: Some(breakout_max_bars),
},
StageConfig {
stage: BalanceBreakoutStage::Aftermath,
max_bars: None,
},
];
ScenarioStateMachine::new(
stages,
vec![BalanceBreakoutStage::Aftermath],
BalanceBreakoutStage::ArmedBalance,
started_at,
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PullbackOutcomeStage {
Direct,
Pullback,
Failure,
}
pub fn direct_pullback_failure_machine(
max_bars: u32,
started_at: i64,
) -> ScenarioStateMachine<PullbackOutcomeStage> {
let stages = [
PullbackOutcomeStage::Direct,
PullbackOutcomeStage::Pullback,
PullbackOutcomeStage::Failure,
]
.into_iter()
.map(|stage| StageConfig {
stage,
max_bars: Some(max_bars),
})
.collect();
ScenarioStateMachine::new(
stages,
vec![
PullbackOutcomeStage::Direct,
PullbackOutcomeStage::Pullback,
PullbackOutcomeStage::Failure,
],
PullbackOutcomeStage::Direct,
started_at,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_advances_through_stages_and_completes() {
let mut machine = edge_setup_watch_trigger_machine(10, 0);
assert_eq!(machine.current_stage(), EdgeSetupStage::Edge);
assert_eq!(machine.status(), ScenarioStatus::Active);
assert!(machine.advance(EdgeSetupStage::Setup, 60));
assert!(machine.advance(EdgeSetupStage::Watch, 120));
assert!(machine.advance(EdgeSetupStage::Trigger, 180));
assert_eq!(machine.status(), ScenarioStatus::Completed);
assert_eq!(machine.history().len(), 4);
assert!(!machine.advance(EdgeSetupStage::Edge, 240));
}
#[test]
fn test_expires_after_max_bars_in_stage() {
let mut machine = edge_setup_watch_trigger_machine(3, 0);
for _ in 0..3 {
machine.tick();
assert_eq!(machine.status(), ScenarioStatus::Active);
}
machine.tick();
assert_eq!(machine.status(), ScenarioStatus::Expired);
}
#[test]
fn test_tick_resets_on_advance() {
let mut machine = edge_setup_watch_trigger_machine(2, 0);
machine.tick();
machine.tick();
assert!(machine.advance(EdgeSetupStage::Setup, 60));
machine.tick();
machine.tick();
assert_eq!(machine.status(), ScenarioStatus::Active);
}
#[test]
fn test_invalidate_is_terminal_and_blocks_further_advance() {
let mut machine = edge_setup_watch_trigger_machine(10, 0);
machine.advance(EdgeSetupStage::Setup, 60);
machine.invalidate();
assert_eq!(machine.status(), ScenarioStatus::Invalidated);
assert!(!machine.advance(EdgeSetupStage::Watch, 120));
machine.invalidate();
assert_eq!(machine.status(), ScenarioStatus::Invalidated);
}
#[test]
fn test_armed_balance_breakout_aftermath_preset() {
let mut machine = armed_balance_breakout_aftermath_machine(5, 3, 0);
assert!(machine.advance(BalanceBreakoutStage::Breakout, 60));
assert!(machine.advance(BalanceBreakoutStage::Aftermath, 120));
assert_eq!(machine.status(), ScenarioStatus::Completed);
}
#[test]
fn test_direct_pullback_failure_preset_each_branch_is_terminal() {
let mut machine = direct_pullback_failure_machine(5, 0);
assert!(machine.advance(PullbackOutcomeStage::Failure, 60));
assert_eq!(machine.status(), ScenarioStatus::Completed);
}
}