1use std::fmt;
4use std::num::NonZeroU32;
5
6use serde::{Deserialize, Deserializer, Serialize};
7
8pub const MAX_SERIES_ID_BYTES: usize = 64;
9pub const MAX_WARMUP_BARS: usize = 1_000_000;
10pub const MAX_DECISION_RECORDS: usize = 1_000_000;
11pub const MAX_SIGNALS_PER_CALLBACK: usize = 4096;
12pub const MAX_REASON_BYTES: usize = 4096;
13
14#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
16pub enum StrategyConfigError {
17 #[error("{field} must be greater than zero")]
18 ZeroValue { field: &'static str },
19 #[error("series ID must contain 1 to {MAX_SERIES_ID_BYTES} ASCII identifier bytes")]
20 InvalidSeriesId,
21 #[error("warmup bars {value} exceed the maximum {MAX_WARMUP_BARS}")]
22 WarmupTooLarge { value: usize },
23 #[error("{field} {value} exceeds the maximum {maximum}")]
24 LimitTooLarge {
25 field: &'static str,
26 value: usize,
27 maximum: usize,
28 },
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
33#[serde(transparent)]
34pub struct SeriesId(String);
35
36impl SeriesId {
37 pub fn new(value: impl Into<String>) -> Result<Self, StrategyConfigError> {
38 let value = value.into();
39 if valid_identifier(&value, MAX_SERIES_ID_BYTES) {
40 Ok(Self(value))
41 } else {
42 Err(StrategyConfigError::InvalidSeriesId)
43 }
44 }
45
46 pub fn as_str(&self) -> &str {
47 &self.0
48 }
49}
50
51impl fmt::Display for SeriesId {
52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 formatter.write_str(&self.0)
54 }
55}
56
57impl<'de> Deserialize<'de> for SeriesId {
58 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
59 where
60 D: Deserializer<'de>,
61 {
62 let value = String::deserialize(deserializer)?;
63 Self::new(value).map_err(serde::de::Error::custom)
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum Timeframe {
71 Seconds(NonZeroU32),
72 Minutes(NonZeroU32),
73 Hours(NonZeroU32),
74 Days(NonZeroU32),
75}
76
77impl Timeframe {
78 pub fn seconds(value: u32) -> Result<Self, StrategyConfigError> {
79 nonzero(value, "timeframe seconds").map(Self::Seconds)
80 }
81
82 pub fn minutes(value: u32) -> Result<Self, StrategyConfigError> {
83 nonzero(value, "timeframe minutes").map(Self::Minutes)
84 }
85
86 pub fn hours(value: u32) -> Result<Self, StrategyConfigError> {
87 nonzero(value, "timeframe hours").map(Self::Hours)
88 }
89
90 pub fn days(value: u32) -> Result<Self, StrategyConfigError> {
91 nonzero(value, "timeframe days").map(Self::Days)
92 }
93
94 pub fn duration_seconds(self) -> u64 {
95 match self {
96 Self::Seconds(value) => u64::from(value.get()),
97 Self::Minutes(value) => u64::from(value.get()) * 60,
98 Self::Hours(value) => u64::from(value.get()) * 60 * 60,
99 Self::Days(value) => u64::from(value.get()) * 24 * 60 * 60,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
106#[serde(rename_all = "snake_case")]
107pub enum PriceBasis {
108 Bid,
109 Ask,
110 Mid,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
115#[serde(transparent)]
116pub struct WarmupRequirement(usize);
117
118impl WarmupRequirement {
119 pub fn bars(value: usize) -> Result<Self, StrategyConfigError> {
120 if value <= MAX_WARMUP_BARS {
121 Ok(Self(value))
122 } else {
123 Err(StrategyConfigError::WarmupTooLarge { value })
124 }
125 }
126
127 pub fn required_bars(self) -> usize {
128 self.0
129 }
130}
131
132impl<'de> Deserialize<'de> for WarmupRequirement {
133 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
134 where
135 D: Deserializer<'de>,
136 {
137 let value = usize::deserialize(deserializer)?;
138 Self::bars(value).map_err(serde::de::Error::custom)
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
144pub struct StrategyRetentionLimits {
145 max_decisions: usize,
146 max_signals_per_callback: usize,
147 max_reason_bytes: usize,
148}
149
150impl StrategyRetentionLimits {
151 pub fn new(
152 max_decisions: usize,
153 max_signals_per_callback: usize,
154 max_reason_bytes: usize,
155 ) -> Result<Self, StrategyConfigError> {
156 validate_limit("max_decisions", max_decisions, MAX_DECISION_RECORDS, true)?;
157 validate_limit(
158 "max_signals_per_callback",
159 max_signals_per_callback,
160 MAX_SIGNALS_PER_CALLBACK,
161 false,
162 )?;
163 validate_limit(
164 "max_reason_bytes",
165 max_reason_bytes,
166 MAX_REASON_BYTES,
167 false,
168 )?;
169 Ok(Self {
170 max_decisions,
171 max_signals_per_callback,
172 max_reason_bytes,
173 })
174 }
175
176 pub fn max_decisions(self) -> usize {
177 self.max_decisions
178 }
179
180 pub fn max_signals_per_callback(self) -> usize {
181 self.max_signals_per_callback
182 }
183
184 pub fn max_reason_bytes(self) -> usize {
185 self.max_reason_bytes
186 }
187}
188
189impl Default for StrategyRetentionLimits {
190 fn default() -> Self {
191 Self {
192 max_decisions: 10_000,
193 max_signals_per_callback: 256,
194 max_reason_bytes: 1024,
195 }
196 }
197}
198
199#[derive(Deserialize)]
200#[serde(deny_unknown_fields)]
201struct StrategyRetentionLimitsDef {
202 max_decisions: usize,
203 max_signals_per_callback: usize,
204 max_reason_bytes: usize,
205}
206
207impl<'de> Deserialize<'de> for StrategyRetentionLimits {
208 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
209 where
210 D: Deserializer<'de>,
211 {
212 let value = StrategyRetentionLimitsDef::deserialize(deserializer)?;
213 Self::new(
214 value.max_decisions,
215 value.max_signals_per_callback,
216 value.max_reason_bytes,
217 )
218 .map_err(serde::de::Error::custom)
219 }
220}
221
222fn nonzero(value: u32, field: &'static str) -> Result<NonZeroU32, StrategyConfigError> {
223 NonZeroU32::new(value).ok_or(StrategyConfigError::ZeroValue { field })
224}
225
226fn validate_limit(
227 field: &'static str,
228 value: usize,
229 maximum: usize,
230 allow_zero: bool,
231) -> Result<(), StrategyConfigError> {
232 if value == 0 && !allow_zero {
233 return Err(StrategyConfigError::ZeroValue { field });
234 }
235 if value > maximum {
236 return Err(StrategyConfigError::LimitTooLarge {
237 field,
238 value,
239 maximum,
240 });
241 }
242 Ok(())
243}
244
245fn valid_identifier(value: &str, maximum: usize) -> bool {
246 !value.is_empty()
247 && value.len() <= maximum
248 && value
249 .bytes()
250 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
251}