1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ScenarioStatus {
9 Active,
10 Expired,
11 Invalidated,
12 Completed,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct StageConfig<S> {
20 pub stage: S,
21 pub max_bars: Option<u32>,
22}
23
24pub struct ScenarioStateMachine<S: Copy + PartialEq> {
26 stages: Vec<StageConfig<S>>,
27 terminal_stages: Vec<S>,
28 current: S,
29 bars_in_stage: u32,
30 status: ScenarioStatus,
31 history: Vec<(S, i64)>,
32}
33
34impl<S: Copy + PartialEq> ScenarioStateMachine<S> {
35 pub fn new(
36 stages: Vec<StageConfig<S>>,
37 terminal_stages: Vec<S>,
38 initial: S,
39 started_at: i64,
40 ) -> Self {
41 Self {
42 stages,
43 terminal_stages,
44 current: initial,
45 bars_in_stage: 0,
46 status: ScenarioStatus::Active,
47 history: vec![(initial, started_at)],
48 }
49 }
50
51 pub fn status(&self) -> ScenarioStatus {
52 self.status
53 }
54
55 pub fn current_stage(&self) -> S {
56 self.current
57 }
58
59 pub fn history(&self) -> &[(S, i64)] {
61 &self.history
62 }
63
64 pub fn bars_in_stage(&self) -> u32 {
65 self.bars_in_stage
66 }
67
68 pub fn tick(&mut self) {
71 if self.status != ScenarioStatus::Active {
72 return;
73 }
74 self.bars_in_stage += 1;
75 if let Some(cfg) = self.stages.iter().find(|c| c.stage == self.current) {
76 if let Some(max) = cfg.max_bars {
77 if self.bars_in_stage > max {
78 self.status = ScenarioStatus::Expired;
79 }
80 }
81 }
82 }
83
84 pub fn advance(&mut self, next_stage: S, timestamp: i64) -> bool {
88 if self.status != ScenarioStatus::Active {
89 return false;
90 }
91 self.current = next_stage;
92 self.bars_in_stage = 0;
93 self.history.push((next_stage, timestamp));
94 if self.terminal_stages.contains(&next_stage) {
95 self.status = ScenarioStatus::Completed;
96 }
97 true
98 }
99
100 pub fn invalidate(&mut self) {
103 if self.status == ScenarioStatus::Active {
104 self.status = ScenarioStatus::Invalidated;
105 }
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum EdgeSetupStage {
115 Edge,
116 Setup,
117 Watch,
118 Trigger,
119}
120
121pub fn edge_setup_watch_trigger_machine(
123 max_bars_per_stage: u32,
124 started_at: i64,
125) -> ScenarioStateMachine<EdgeSetupStage> {
126 let stages = [
127 EdgeSetupStage::Edge,
128 EdgeSetupStage::Setup,
129 EdgeSetupStage::Watch,
130 EdgeSetupStage::Trigger,
131 ]
132 .into_iter()
133 .map(|stage| StageConfig {
134 stage,
135 max_bars: Some(max_bars_per_stage),
136 })
137 .collect();
138 ScenarioStateMachine::new(
139 stages,
140 vec![EdgeSetupStage::Trigger],
141 EdgeSetupStage::Edge,
142 started_at,
143 )
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum BalanceBreakoutStage {
148 ArmedBalance,
149 Breakout,
150 Aftermath,
151}
152
153pub fn armed_balance_breakout_aftermath_machine(
155 armed_max_bars: u32,
156 breakout_max_bars: u32,
157 started_at: i64,
158) -> ScenarioStateMachine<BalanceBreakoutStage> {
159 let stages = vec![
160 StageConfig {
161 stage: BalanceBreakoutStage::ArmedBalance,
162 max_bars: Some(armed_max_bars),
163 },
164 StageConfig {
165 stage: BalanceBreakoutStage::Breakout,
166 max_bars: Some(breakout_max_bars),
167 },
168 StageConfig {
169 stage: BalanceBreakoutStage::Aftermath,
170 max_bars: None,
171 },
172 ];
173 ScenarioStateMachine::new(
174 stages,
175 vec![BalanceBreakoutStage::Aftermath],
176 BalanceBreakoutStage::ArmedBalance,
177 started_at,
178 )
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum PullbackOutcomeStage {
183 Direct,
184 Pullback,
185 Failure,
186}
187
188pub fn direct_pullback_failure_machine(
191 max_bars: u32,
192 started_at: i64,
193) -> ScenarioStateMachine<PullbackOutcomeStage> {
194 let stages = [
195 PullbackOutcomeStage::Direct,
196 PullbackOutcomeStage::Pullback,
197 PullbackOutcomeStage::Failure,
198 ]
199 .into_iter()
200 .map(|stage| StageConfig {
201 stage,
202 max_bars: Some(max_bars),
203 })
204 .collect();
205 ScenarioStateMachine::new(
206 stages,
207 vec![
208 PullbackOutcomeStage::Direct,
209 PullbackOutcomeStage::Pullback,
210 PullbackOutcomeStage::Failure,
211 ],
212 PullbackOutcomeStage::Direct,
213 started_at,
214 )
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_advances_through_stages_and_completes() {
223 let mut machine = edge_setup_watch_trigger_machine(10, 0);
224 assert_eq!(machine.current_stage(), EdgeSetupStage::Edge);
225 assert_eq!(machine.status(), ScenarioStatus::Active);
226
227 assert!(machine.advance(EdgeSetupStage::Setup, 60));
228 assert!(machine.advance(EdgeSetupStage::Watch, 120));
229 assert!(machine.advance(EdgeSetupStage::Trigger, 180));
230
231 assert_eq!(machine.status(), ScenarioStatus::Completed);
232 assert_eq!(machine.history().len(), 4);
233 assert!(!machine.advance(EdgeSetupStage::Edge, 240));
235 }
236
237 #[test]
238 fn test_expires_after_max_bars_in_stage() {
239 let mut machine = edge_setup_watch_trigger_machine(3, 0);
240 for _ in 0..3 {
241 machine.tick();
242 assert_eq!(machine.status(), ScenarioStatus::Active);
243 }
244 machine.tick();
245 assert_eq!(machine.status(), ScenarioStatus::Expired);
246 }
247
248 #[test]
249 fn test_tick_resets_on_advance() {
250 let mut machine = edge_setup_watch_trigger_machine(2, 0);
251 machine.tick();
252 machine.tick();
253 assert!(machine.advance(EdgeSetupStage::Setup, 60));
254 machine.tick();
256 machine.tick();
257 assert_eq!(machine.status(), ScenarioStatus::Active);
258 }
259
260 #[test]
261 fn test_invalidate_is_terminal_and_blocks_further_advance() {
262 let mut machine = edge_setup_watch_trigger_machine(10, 0);
263 machine.advance(EdgeSetupStage::Setup, 60);
264 machine.invalidate();
265 assert_eq!(machine.status(), ScenarioStatus::Invalidated);
266 assert!(!machine.advance(EdgeSetupStage::Watch, 120));
267 machine.invalidate();
269 assert_eq!(machine.status(), ScenarioStatus::Invalidated);
270 }
271
272 #[test]
273 fn test_armed_balance_breakout_aftermath_preset() {
274 let mut machine = armed_balance_breakout_aftermath_machine(5, 3, 0);
275 assert!(machine.advance(BalanceBreakoutStage::Breakout, 60));
276 assert!(machine.advance(BalanceBreakoutStage::Aftermath, 120));
277 assert_eq!(machine.status(), ScenarioStatus::Completed);
278 }
279
280 #[test]
281 fn test_direct_pullback_failure_preset_each_branch_is_terminal() {
282 let mut machine = direct_pullback_failure_machine(5, 0);
283 assert!(machine.advance(PullbackOutcomeStage::Failure, 60));
284 assert_eq!(machine.status(), ScenarioStatus::Completed);
285 }
286}