1use std::collections::BTreeMap;
2use std::fmt;
3
4use qs_core::OrderType;
5use serde::{Deserialize, Deserializer, Serialize};
6
7use crate::{Literal, ScalarType, ValueType, validate_id};
8
9pub const MAX_SOURCES: usize = 32;
10pub const MAX_COMPLETED_BARS: usize = 32;
11pub const MAX_MATERIALS: usize = 128;
12pub const MAX_MATERIAL_INPUTS: usize = 16;
13pub const MAX_MATERIAL_ARGS: usize = 32;
14pub const MAX_PARAMETERS: usize = 64;
15pub const MAX_PARAMETER_OPTIONS: usize = 64;
16pub const MAX_MATERIAL_LOOKBACK: usize = 4096;
17pub const MAX_MATERIAL_STATE_BYTES: usize = 65_536;
18pub const MAX_STATES: usize = 64;
19pub const MAX_VARIABLES: usize = 64;
20pub const MAX_TRANSITIONS: usize = 64;
21pub const MAX_ACTIONS: usize = 16;
22pub const MAX_ASSIGNMENTS: usize = 32;
23pub const MAX_ENTRY_TARGETS: usize = 16;
24pub const MAX_NOTES: usize = 16;
25pub const MAX_NAMED_VALUES: usize = 32;
26pub const MAX_EXPR_NODES: usize = 512;
27pub const MAX_EXPR_DEPTH: usize = 32;
28pub const MAX_LEGS: usize = 16;
29pub const MAX_OUTPUT_COMMANDS: usize = 16;
30pub const MAX_OUTPUT_NOTES: usize = 16;
31pub const MAX_COMMAND_CORRELATIONS: usize = 64;
32pub const MAX_PENDING_FEEDBACK: usize = 64;
33
34#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
36#[serde(transparent)]
37pub struct SourceId(String);
38
39impl SourceId {
40 pub fn new(value: impl Into<String>) -> Result<Self, String> {
41 let value = value.into();
42 validate_id(&value)?;
43 Ok(Self(value))
44 }
45
46 pub fn as_str(&self) -> &str {
47 &self.0
48 }
49}
50
51impl fmt::Display for SourceId {
52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 formatter.write_str(&self.0)
54 }
55}
56
57impl<'de> Deserialize<'de> for SourceId {
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, PartialEq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct StrategyConfig {
71 pub strategy_id: String,
72 pub title: String,
73 #[serde(default)]
74 pub parameters: Vec<ParameterConfig>,
75 pub initial_state: String,
76 pub sources: Vec<SourceId>,
77 pub trade_slots: Vec<String>,
78 #[serde(default)]
79 pub materials: Vec<MaterialConfig>,
80 #[serde(default)]
81 pub variables: Vec<VariableConfig>,
82 pub states: Vec<StateConfig>,
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86#[serde(deny_unknown_fields)]
87pub struct MaterialConfig {
88 pub id: String,
89 pub key: String,
90 #[serde(default)]
91 pub inputs: Vec<Expr>,
92 #[serde(default)]
93 pub params: MaterialArgs,
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98#[serde(
99 tag = "type",
100 content = "value",
101 rename_all = "snake_case",
102 deny_unknown_fields
103)]
104pub enum MaterialArg {
105 Integer(i64),
106 Number(f64),
107 Source(SourceId),
108 Slot(String),
109 BarField(BarField),
110 ActionKind(ConfiguredActionKind),
111 Param(String),
112}
113
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
116#[serde(transparent)]
117pub struct MaterialArgs(pub BTreeMap<String, MaterialArg>);
118
119impl MaterialArgs {
120 pub fn new(values: impl IntoIterator<Item = (impl Into<String>, MaterialArg)>) -> Self {
121 Self(
122 values
123 .into_iter()
124 .map(|(name, value)| (name.into(), value))
125 .collect(),
126 )
127 }
128
129 pub fn get(&self, name: &str) -> Option<&MaterialArg> {
130 self.0.get(name)
131 }
132
133 pub fn is_empty(&self) -> bool {
134 self.0.is_empty()
135 }
136
137 pub fn len(&self) -> usize {
138 self.0.len()
139 }
140
141 pub fn iter(&self) -> impl Iterator<Item = (&String, &MaterialArg)> {
142 self.0.iter()
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Default)]
148pub enum MaterialParams {
149 #[default]
150 None,
151 BarField {
152 source: SourceId,
153 field: BarField,
154 },
155 Ema {
156 period: u16,
157 },
158 Atr {
159 source: SourceId,
160 period: u16,
161 },
162 Position {
163 slot: String,
164 },
165 Feedback {
166 slot: String,
167 action: ConfiguredActionKind,
168 },
169}
170
171impl From<MaterialParams> for MaterialArgs {
172 fn from(value: MaterialParams) -> Self {
173 match value {
174 MaterialParams::None => Self::default(),
175 MaterialParams::BarField { source, field } => Self::new([
176 ("source", MaterialArg::Source(source)),
177 ("field", MaterialArg::BarField(field)),
178 ]),
179 MaterialParams::Ema { period } => {
180 Self::new([("period", MaterialArg::Integer(i64::from(period)))])
181 }
182 MaterialParams::Atr { source, period } => Self::new([
183 ("source", MaterialArg::Source(source)),
184 ("period", MaterialArg::Integer(i64::from(period))),
185 ]),
186 MaterialParams::Position { slot } => Self::new([("slot", MaterialArg::Slot(slot))]),
187 MaterialParams::Feedback { slot, action } => Self::new([
188 ("slot", MaterialArg::Slot(slot)),
189 ("action", MaterialArg::ActionKind(action)),
190 ]),
191 }
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
197pub enum ParameterKind {
198 Integer,
199 Number,
200 Choice { options: Vec<String> },
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(deny_unknown_fields)]
205pub struct ParameterConfig {
206 pub id: String,
207 pub kind: ParameterKind,
208}
209
210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211#[serde(
212 tag = "type",
213 content = "value",
214 rename_all = "snake_case",
215 deny_unknown_fields
216)]
217pub enum ParameterValue {
218 Integer(i64),
219 Number(f64),
220 Choice(String),
221}
222
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
224#[serde(transparent)]
225pub struct ParameterBinding(pub BTreeMap<String, ParameterValue>);
226
227impl ParameterBinding {
228 pub fn new(values: impl IntoIterator<Item = (impl Into<String>, ParameterValue)>) -> Self {
229 Self(
230 values
231 .into_iter()
232 .map(|(name, value)| (name.into(), value))
233 .collect(),
234 )
235 }
236
237 pub fn get(&self, name: &str) -> Option<&ParameterValue> {
238 self.0.get(name)
239 }
240
241 pub fn iter(&self) -> impl Iterator<Item = (&String, &ParameterValue)> {
242 self.0.iter()
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
247#[serde(rename_all = "snake_case")]
248pub enum BarField {
249 Open,
250 High,
251 Low,
252 Close,
253 Volume,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(rename_all = "snake_case")]
258pub enum PositionField {
259 Exists,
260 IsPending,
261 IsOpen,
262 EntryPrice,
263 Side,
264 RemainingSize,
265 Stoploss,
266 OpenedAt,
267 FavorableExcursion,
268 AdverseExcursion,
269 InitialRisk,
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
273#[serde(rename_all = "snake_case")]
274pub enum FeedbackField {
275 EntryFilled,
276 EntryRejected,
277 PositionClosed,
278 CancellationApplied,
279 CancellationRejected,
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(rename_all = "snake_case")]
284pub enum ConfiguredActionKind {
285 Entry,
286 Close,
287 ClosePartial,
288 MoveStoplossToEntry,
289 ModifyStoploss,
290 CancelPending,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(rename_all = "snake_case")]
295pub enum DecisionKind {
296 Entry,
297 Management,
298 Exit,
299 Observation,
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
303#[serde(rename_all = "snake_case")]
304pub enum NoteKind {
305 Observation,
306 Risk,
307 Execution,
308 Lifecycle,
309}
310
311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
312#[serde(deny_unknown_fields)]
313pub struct VariableConfig {
314 pub id: String,
315 pub value_type: ValueType,
316 pub initial: Literal,
317}
318
319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
320#[serde(deny_unknown_fields)]
321pub struct StateConfig {
322 pub id: String,
323 #[serde(default)]
324 pub transitions: Vec<TransitionConfig>,
325}
326
327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328#[serde(deny_unknown_fields)]
329pub struct TransitionConfig {
330 pub priority: i32,
331 pub target: String,
332 pub when: Expr,
333 #[serde(default)]
334 pub assignments: Vec<AssignmentConfig>,
335 pub decision: Option<DecisionTemplate>,
336 #[serde(default)]
337 pub actions: Vec<ActionTemplate>,
338 #[serde(default)]
339 pub notes: Vec<NoteTemplate>,
340}
341
342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
343#[serde(deny_unknown_fields)]
344pub struct AssignmentConfig {
345 pub variable: String,
346 pub value: Expr,
347}
348
349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
350#[serde(deny_unknown_fields)]
351pub struct DecisionTemplate {
352 pub kind: DecisionKind,
353 pub reason: String,
354 pub trade_slot: Option<String>,
355 #[serde(default)]
356 pub values: Vec<NamedExpr>,
357}
358
359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
360#[serde(deny_unknown_fields)]
361pub struct NoteTemplate {
362 pub kind: NoteKind,
363 pub reason: String,
364 pub trade_slot: Option<String>,
365 #[serde(default)]
366 pub values: Vec<NamedExpr>,
367}
368
369#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
370#[serde(deny_unknown_fields)]
371pub struct NamedExpr {
372 pub name: String,
373 pub value: Expr,
374}
375
376#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
377#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)]
378pub enum Expr {
379 Literal {
380 value: Literal,
381 },
382 Variable {
383 id: String,
384 },
385 Material {
386 id: String,
387 },
388 Param {
389 id: String,
390 },
391 Select {
392 param: String,
393 cases: BTreeMap<String, Expr>,
394 },
395 Input {
396 field: String,
397 value_type: ValueType,
398 },
399 Bar {
400 source: SourceId,
401 field: BarField,
402 },
403 Position {
404 slot: String,
405 field: PositionField,
406 },
407 Feedback {
408 slot: String,
409 action: ConfiguredActionKind,
410 field: FeedbackField,
411 },
412 InputTime,
413 Readiness,
414 Eq {
415 left: Box<Expr>,
416 right: Box<Expr>,
417 },
418 Ne {
419 left: Box<Expr>,
420 right: Box<Expr>,
421 },
422 Lt {
423 left: Box<Expr>,
424 right: Box<Expr>,
425 },
426 Le {
427 left: Box<Expr>,
428 right: Box<Expr>,
429 },
430 Gt {
431 left: Box<Expr>,
432 right: Box<Expr>,
433 },
434 Ge {
435 left: Box<Expr>,
436 right: Box<Expr>,
437 },
438 All {
439 items: Vec<Expr>,
440 },
441 Any {
442 items: Vec<Expr>,
443 },
444 Not {
445 value: Box<Expr>,
446 },
447 Strict {
449 value: Box<Expr>,
450 },
451 Add {
452 left: Box<Expr>,
453 right: Box<Expr>,
454 },
455 Sub {
456 left: Box<Expr>,
457 right: Box<Expr>,
458 },
459 Mul {
460 left: Box<Expr>,
461 right: Box<Expr>,
462 },
463 Div {
464 left: Box<Expr>,
465 right: Box<Expr>,
466 },
467 Min {
468 left: Box<Expr>,
469 right: Box<Expr>,
470 },
471 Max {
472 left: Box<Expr>,
473 right: Box<Expr>,
474 },
475 Abs {
476 value: Box<Expr>,
477 },
478 IsPresent {
479 value: Box<Expr>,
480 },
481 IsMissing {
482 value: Box<Expr>,
483 },
484}
485
486#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
487#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
488pub enum ActionTemplate {
489 Entry {
490 slot: String,
491 side: Expr,
492 order_type: OrderType,
493 price: Expr,
494 risk: Expr,
495 stoploss: Expr,
496 #[serde(default)]
497 targets: Vec<Expr>,
498 #[serde(default, skip_serializing_if = "Option::is_none")]
500 entry_class: Option<String>,
501 },
502 Close {
503 slot: String,
504 },
505 ClosePartial {
506 slot: String,
507 ratio: Expr,
508 },
509 MoveStoplossToEntry {
510 slot: String,
511 },
512 ModifyStoploss {
513 slot: String,
514 price: Expr,
515 },
516 CancelPending {
517 slot: String,
518 },
519}
520
521pub(crate) fn bar_field_type(field: BarField) -> ValueType {
522 match field {
523 BarField::Volume => ValueType::optional(ScalarType::Number),
524 _ => ValueType::optional(ScalarType::Price),
525 }
526}