1use chrono::NaiveDateTime;
4
5use crate::data_feed::FeedEvent;
6use crate::profile::RawSignal;
7
8use super::domain::validate_decision_fields;
9use super::{
10 ClosedBar, StrategyContext, StrategyDecisionKind, StrategyDecisionRecord, StrategyDescriptor,
11 StrategyDomainError, StrategyFeedback, StrategyJournalDraft, StrategyJournalError,
12 StrategyObservation, StrategyRequirements, StrategyRetentionLimits,
13};
14
15#[derive(Debug, Clone, Copy)]
17pub struct StrategyEvent<'a> {
18 primary_events: &'a [FeedEvent],
19 closed_bars: &'a [ClosedBar],
20 observations: &'a [StrategyObservation],
21 feedback: StrategyFeedback<'a>,
22}
23
24impl<'a> StrategyEvent<'a> {
25 pub const fn new(
26 primary_events: &'a [FeedEvent],
27 closed_bars: &'a [ClosedBar],
28 observations: &'a [StrategyObservation],
29 feedback: StrategyFeedback<'a>,
30 ) -> Self {
31 Self {
32 primary_events,
33 closed_bars,
34 observations,
35 feedback,
36 }
37 }
38
39 pub const fn primary_events(self) -> &'a [FeedEvent] {
40 self.primary_events
41 }
42
43 pub const fn closed_bars(self) -> &'a [ClosedBar] {
44 self.closed_bars
45 }
46
47 pub const fn observations(self) -> &'a [StrategyObservation] {
48 self.observations
49 }
50
51 pub const fn feedback(self) -> StrategyFeedback<'a> {
52 self.feedback
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct StrategyDecisionDraft {
59 kind: StrategyDecisionKind,
60 reason: String,
61 related_trade_id: Option<String>,
62 signals: Vec<RawSignal>,
63}
64
65impl StrategyDecisionDraft {
66 pub fn new(
67 kind: StrategyDecisionKind,
68 reason: impl Into<String>,
69 related_trade_id: Option<String>,
70 signals: Vec<RawSignal>,
71 limits: StrategyRetentionLimits,
72 ) -> Result<Self, StrategyDomainError> {
73 let reason = reason.into();
74 validate_decision_fields(&reason, related_trade_id.as_deref(), signals.len(), limits)?;
75 Ok(Self {
76 kind,
77 reason,
78 related_trade_id,
79 signals,
80 })
81 }
82
83 pub fn kind(&self) -> StrategyDecisionKind {
84 self.kind
85 }
86
87 pub fn reason(&self) -> &str {
88 &self.reason
89 }
90
91 pub fn related_trade_id(&self) -> Option<&str> {
92 self.related_trade_id.as_deref()
93 }
94
95 pub fn signals(&self) -> &[RawSignal] {
96 &self.signals
97 }
98
99 pub fn into_record(
100 self,
101 sequence: u64,
102 observed_through: NaiveDateTime,
103 limits: StrategyRetentionLimits,
104 ) -> Result<StrategyDecisionRecord, StrategyRuntimeError> {
105 if let Some((signal_index, signal)) = self
106 .signals
107 .iter()
108 .enumerate()
109 .find(|(_, signal)| signal.ts() != observed_through)
110 {
111 return Err(StrategyRuntimeError::SignalTimestampMismatch {
112 signal_index,
113 signal_ts: signal.ts(),
114 observed_through,
115 });
116 }
117 Ok(StrategyDecisionRecord::new(
118 sequence,
119 observed_through,
120 self.kind,
121 self.reason,
122 self.related_trade_id,
123 self.signals,
124 limits,
125 )?)
126 }
127}
128
129#[derive(Debug, Clone, Default)]
131pub struct StrategyOutput {
132 decision: Option<StrategyDecisionDraft>,
133 journal: Vec<StrategyJournalDraft>,
134}
135
136impl StrategyOutput {
137 pub const fn new(decision: Option<StrategyDecisionDraft>) -> Self {
138 Self {
139 decision,
140 journal: Vec::new(),
141 }
142 }
143
144 pub const fn none() -> Self {
145 Self::new(None)
146 }
147
148 pub fn from_decision(decision: StrategyDecisionDraft) -> Self {
149 Self::new(Some(decision))
150 }
151
152 pub fn from_journal(journal: Vec<StrategyJournalDraft>) -> Self {
153 Self {
154 decision: None,
155 journal,
156 }
157 }
158
159 pub fn with_journal(mut self, journal: Vec<StrategyJournalDraft>) -> Self {
160 self.journal = journal;
161 self
162 }
163
164 pub fn decision(&self) -> Option<&StrategyDecisionDraft> {
165 self.decision.as_ref()
166 }
167
168 pub fn journal(&self) -> &[StrategyJournalDraft] {
169 &self.journal
170 }
171
172 pub fn into_decision(self) -> Option<StrategyDecisionDraft> {
173 self.decision
174 }
175
176 pub fn into_parts(self) -> (Option<StrategyDecisionDraft>, Vec<StrategyJournalDraft>) {
177 (self.decision, self.journal)
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
183pub enum StrategyRuntimeError {
184 #[error(transparent)]
185 Domain(#[from] StrategyDomainError),
186 #[error(transparent)]
187 Journal(#[from] StrategyJournalError),
188 #[error(
189 "signal {signal_index} timestamp {signal_ts} does not match strategy boundary {observed_through}"
190 )]
191 SignalTimestampMismatch {
192 signal_index: usize,
193 signal_ts: NaiveDateTime,
194 observed_through: NaiveDateTime,
195 },
196}
197
198pub trait HistoricalStrategy {
200 type Error;
201
202 fn descriptor(&self) -> &StrategyDescriptor;
203 fn requirements(&self) -> &StrategyRequirements;
204
205 fn on_event(
206 &mut self,
207 event: StrategyEvent<'_>,
208 context: StrategyContext<'_>,
209 ) -> Result<StrategyOutput, Self::Error>;
210}