Skip to main content

simulator_client/
transaction_step.rs

1use std::time::Duration;
2
3use simulator_api::{
4    BacktestRequest, BacktestResponse, ContinueParams, ContinueToParams, DiscoveryBatchEvent,
5    PausedEvent,
6};
7
8use crate::{BacktestClientError, BacktestClientResult, BacktestSession};
9
10/// A batch discovery paired with the pause location immediately before it.
11#[derive(Debug, Clone)]
12pub struct DiscoveryPause {
13    /// The discovery event that triggered the pause.
14    pub discovery: DiscoveryBatchEvent,
15    /// Where the session paused; state through `batch_index - 1` is visible via RPC.
16    pub paused: PausedEvent,
17}
18
19/// Result of a single [`BacktestSession::advance_to_discovery`] call.
20#[derive(Debug)]
21pub enum DiscoveryStepResult {
22    /// Session paused before the discovered batch. Inspect state via RPC or
23    /// simulate custom transactions, then call `advance_to_discovery` again.
24    Paused(DiscoveryPause),
25    /// No more discoveries in the session range.
26    Completed,
27}
28
29impl BacktestSession {
30    /// Wait for the next batch matching a registered discovery filter and pause
31    /// immediately before it executes.
32    ///
33    /// `DiscoveryBatch` events are emitted by the server as a background scan
34    /// independent of execution — no `Continue` is needed to trigger them.
35    /// This method consumes the next queued `DiscoveryBatch`, sends `ContinueTo`
36    /// to execute up to (but not including) that batch, then waits for `Paused`.
37    ///
38    /// While paused, the session's RPC endpoint reflects state through
39    /// `batch_index - 1` of the discovered slot — no transaction in the
40    /// discovered batch has executed yet.
41    pub async fn advance_to_discovery(
42        &mut self,
43        offset: Option<u32>,
44        timeout: Option<Duration>,
45    ) -> BacktestClientResult<DiscoveryStepResult> {
46        // Phase 1: fetch next DiscoveryBatch or fail if none arrives within the timeout.
47        let discovery: DiscoveryBatchEvent = loop {
48            let Some(response) = self.next_response(timeout).await? else {
49                return Err(BacktestClientError::Closed {
50                    reason: "websocket closed while waiting for DiscoveryBatch".to_string(),
51                });
52            };
53            match response {
54                BacktestResponse::DiscoveryBatch(event) => break event,
55                BacktestResponse::Completed { .. } => return Ok(DiscoveryStepResult::Completed),
56                BacktestResponse::Error(err) => return Err(BacktestClientError::Remote(err)),
57                // Server is paused waiting for a Continue. Send one so it can advance
58                // to the next discovery or reach end-of-range and emit Completed.
59                BacktestResponse::ReadyForContinue => {
60                    self.send(
61                        &BacktestRequest::Continue(ContinueParams {
62                            advance_count: u64::MAX,
63                            ..Default::default()
64                        }),
65                        timeout,
66                    )
67                    .await?;
68                }
69                other => {
70                    tracing::warn!("ignoring {other:?} while waiting for DiscoveryBatch");
71                }
72            }
73        };
74
75        // Phase 2: execute up to (not including) the discovered batch if offset is 0.
76        // Execute past the discovered batch if offset is > 0.
77        let offset = offset.unwrap_or(0);
78        self.send(
79            &BacktestRequest::ContinueTo(ContinueToParams {
80                slot: discovery.slot,
81                batch_index: Some(discovery.batch_index + offset),
82            }),
83            timeout,
84        )
85        .await?;
86
87        // Phase 3: consume until Paused.
88        // Extra DiscoveryBatch events are held in a local buffer (not self.backlog)
89        // so next_response keeps reading from the websocket. After Paused arrives
90        // they are flushed to self.backlog for the next advance_to_discovery call.
91        let mut pending_discoveries: Vec<BacktestResponse> = Vec::new();
92        let paused: PausedEvent = loop {
93            let Some(response) = self.next_response(timeout).await? else {
94                return Err(BacktestClientError::Closed {
95                    reason: "websocket closed while waiting for Paused after ContinueTo"
96                        .to_string(),
97                });
98            };
99            match response {
100                BacktestResponse::Paused(event) => {
101                    self.ready_for_continue = true;
102                    for d in pending_discoveries {
103                        self.push_backlog(d);
104                    }
105                    break event;
106                }
107                next @ BacktestResponse::DiscoveryBatch(_) => {
108                    pending_discoveries.push(next);
109                }
110                BacktestResponse::SlotNotification(_)
111                | BacktestResponse::Status { .. }
112                | BacktestResponse::Success
113                | BacktestResponse::ReadyForContinue => {}
114                BacktestResponse::Completed { .. } => {
115                    return Ok(DiscoveryStepResult::Completed);
116                }
117                BacktestResponse::Error(err) => return Err(BacktestClientError::Remote(err)),
118                other => {
119                    return Err(BacktestClientError::UnexpectedResponse {
120                        context: "waiting for Paused after ContinueTo",
121                        response: Box::new(other),
122                    });
123                }
124            }
125        };
126
127        Ok(DiscoveryStepResult::Paused(DiscoveryPause {
128            discovery,
129            paused,
130        }))
131    }
132}