1use std::fmt;
2
3use qs_core::OrderType;
4use serde::{Deserialize, Deserializer, Serialize};
5
6use crate::{Literal, ScalarType, ValueType, validate_id};
7
8pub const MAX_SOURCES: usize = 32;
9pub const MAX_COMPLETED_BARS: usize = 32;
10pub const MAX_MATERIALS: usize = 128;
11pub const MAX_MATERIAL_INPUTS: usize = 16;
12pub const MAX_MATERIAL_LOOKBACK: usize = 4096;
13pub const MAX_MATERIAL_STATE_BYTES: usize = 65_536;
14pub const MAX_STATES: usize = 64;
15pub const MAX_VARIABLES: usize = 64;
16pub const MAX_TRANSITIONS: usize = 64;
17pub const MAX_ACTIONS: usize = 16;
18pub const MAX_ASSIGNMENTS: usize = 32;
19pub const MAX_ENTRY_TARGETS: usize = 16;
20pub const MAX_NOTES: usize = 16;
21pub const MAX_NAMED_VALUES: usize = 32;
22pub const MAX_EXPR_NODES: usize = 512;
23pub const MAX_EXPR_DEPTH: usize = 32;
24pub const MAX_LEGS: usize = 16;
25pub const MAX_OUTPUT_COMMANDS: usize = 16;
26pub const MAX_OUTPUT_NOTES: usize = 16;
27pub const MAX_COMMAND_CORRELATIONS: usize = 64;
28pub const MAX_PENDING_FEEDBACK: usize = 64;
29
30#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
32#[serde(transparent)]
33pub struct SourceId(String);
34
35impl SourceId {
36 pub fn new(value: impl Into<String>) -> Result<Self, String> {
37 let value = value.into();
38 validate_id(&value)?;
39 Ok(Self(value))
40 }
41
42 pub fn as_str(&self) -> &str {
43 &self.0
44 }
45}
46
47impl fmt::Display for SourceId {
48 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49 formatter.write_str(&self.0)
50 }
51}
52
53impl<'de> Deserialize<'de> for SourceId {
54 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
55 where
56 D: Deserializer<'de>,
57 {
58 let value = String::deserialize(deserializer)?;
59 Self::new(value).map_err(serde::de::Error::custom)
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65#[serde(deny_unknown_fields)]
66pub struct StrategyConfig {
67 pub strategy_id: String,
68 pub title: String,
69 pub initial_state: String,
70 pub sources: Vec<SourceId>,
71 pub trade_slots: Vec<String>,
72 #[serde(default)]
73 pub materials: Vec<MaterialConfig>,
74 #[serde(default)]
75 pub variables: Vec<VariableConfig>,
76 pub states: Vec<StateConfig>,
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80#[serde(deny_unknown_fields)]
81pub struct MaterialConfig {
82 pub id: String,
83 pub key: String,
84 #[serde(default)]
85 pub inputs: Vec<Expr>,
86 #[serde(default)]
87 pub params: MaterialParams,
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
92#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
93pub enum MaterialParams {
94 #[default]
95 None,
96 BarField {
97 source: SourceId,
98 field: BarField,
99 },
100 Ema {
101 period: u16,
102 },
103 Atr {
104 source: SourceId,
105 period: u16,
106 },
107 Position {
108 slot: String,
109 },
110 Feedback {
111 slot: String,
112 action: ConfiguredActionKind,
113 },
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub enum BarField {
119 Open,
120 High,
121 Low,
122 Close,
123 Volume,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum PositionField {
129 Exists,
130 IsPending,
131 IsOpen,
132 EntryPrice,
133 Side,
134 RemainingSize,
135 Stoploss,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(rename_all = "snake_case")]
140pub enum FeedbackField {
141 EntryFilled,
142 EntryRejected,
143 PositionClosed,
144 CancellationApplied,
145 CancellationRejected,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(rename_all = "snake_case")]
150pub enum ConfiguredActionKind {
151 Entry,
152 Close,
153 ClosePartial,
154 MoveStoplossToEntry,
155 ModifyStoploss,
156 CancelPending,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum DecisionKind {
162 Entry,
163 Management,
164 Exit,
165 Observation,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub enum NoteKind {
171 Observation,
172 Risk,
173 Execution,
174 Lifecycle,
175}
176
177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
178#[serde(deny_unknown_fields)]
179pub struct VariableConfig {
180 pub id: String,
181 pub value_type: ValueType,
182 pub initial: Literal,
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
186#[serde(deny_unknown_fields)]
187pub struct StateConfig {
188 pub id: String,
189 #[serde(default)]
190 pub transitions: Vec<TransitionConfig>,
191}
192
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194#[serde(deny_unknown_fields)]
195pub struct TransitionConfig {
196 pub priority: i32,
197 pub target: String,
198 pub when: Expr,
199 #[serde(default)]
200 pub assignments: Vec<AssignmentConfig>,
201 pub decision: Option<DecisionTemplate>,
202 #[serde(default)]
203 pub actions: Vec<ActionTemplate>,
204 #[serde(default)]
205 pub notes: Vec<NoteTemplate>,
206}
207
208#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
209#[serde(deny_unknown_fields)]
210pub struct AssignmentConfig {
211 pub variable: String,
212 pub value: Expr,
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216#[serde(deny_unknown_fields)]
217pub struct DecisionTemplate {
218 pub kind: DecisionKind,
219 pub reason: String,
220 pub trade_slot: Option<String>,
221 #[serde(default)]
222 pub values: Vec<NamedExpr>,
223}
224
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226#[serde(deny_unknown_fields)]
227pub struct NoteTemplate {
228 pub kind: NoteKind,
229 pub reason: String,
230 pub trade_slot: Option<String>,
231 #[serde(default)]
232 pub values: Vec<NamedExpr>,
233}
234
235#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
236#[serde(deny_unknown_fields)]
237pub struct NamedExpr {
238 pub name: String,
239 pub value: Expr,
240}
241
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
243#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)]
244pub enum Expr {
245 Literal {
246 value: Literal,
247 },
248 Variable {
249 id: String,
250 },
251 Material {
252 id: String,
253 },
254 Input {
255 field: String,
256 value_type: ValueType,
257 },
258 Bar {
259 source: SourceId,
260 field: BarField,
261 },
262 Position {
263 slot: String,
264 field: PositionField,
265 },
266 Feedback {
267 slot: String,
268 action: ConfiguredActionKind,
269 field: FeedbackField,
270 },
271 InputTime,
272 Readiness,
273 Eq {
274 left: Box<Expr>,
275 right: Box<Expr>,
276 },
277 Ne {
278 left: Box<Expr>,
279 right: Box<Expr>,
280 },
281 Lt {
282 left: Box<Expr>,
283 right: Box<Expr>,
284 },
285 Le {
286 left: Box<Expr>,
287 right: Box<Expr>,
288 },
289 Gt {
290 left: Box<Expr>,
291 right: Box<Expr>,
292 },
293 Ge {
294 left: Box<Expr>,
295 right: Box<Expr>,
296 },
297 All {
298 items: Vec<Expr>,
299 },
300 Any {
301 items: Vec<Expr>,
302 },
303 Not {
304 value: Box<Expr>,
305 },
306 Add {
307 left: Box<Expr>,
308 right: Box<Expr>,
309 },
310 Sub {
311 left: Box<Expr>,
312 right: Box<Expr>,
313 },
314 Mul {
315 left: Box<Expr>,
316 right: Box<Expr>,
317 },
318 Div {
319 left: Box<Expr>,
320 right: Box<Expr>,
321 },
322 Min {
323 left: Box<Expr>,
324 right: Box<Expr>,
325 },
326 Max {
327 left: Box<Expr>,
328 right: Box<Expr>,
329 },
330 Abs {
331 value: Box<Expr>,
332 },
333 IsPresent {
334 value: Box<Expr>,
335 },
336 IsMissing {
337 value: Box<Expr>,
338 },
339}
340
341#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
342#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
343pub enum ActionTemplate {
344 Entry {
345 slot: String,
346 side: Expr,
347 order_type: OrderType,
348 price: Expr,
349 risk: Expr,
350 stoploss: Expr,
351 #[serde(default)]
352 targets: Vec<Expr>,
353 },
354 Close {
355 slot: String,
356 },
357 ClosePartial {
358 slot: String,
359 ratio: Expr,
360 },
361 MoveStoplossToEntry {
362 slot: String,
363 },
364 ModifyStoploss {
365 slot: String,
366 price: Expr,
367 },
368 CancelPending {
369 slot: String,
370 },
371}
372
373pub(crate) fn bar_field_type(field: BarField) -> ValueType {
374 match field {
375 BarField::Volume => ValueType::optional(ScalarType::Number),
376 _ => ValueType::optional(ScalarType::Price),
377 }
378}