Skip to main content

qs_strategy/
template.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use chrono::NaiveDateTime;
4use serde::{Deserialize, Serialize};
5
6use crate::expression::{EvalScope, ExprScope, collect_material_refs, compile_expr};
7use crate::{
8    ActionTemplate, BarField, CompileError, ConfiguredActionKind, ConfiguredStrategy, Expr,
9    MaterialArg, MaterialArgs, MaterialLibrary, ParamKind, ParameterBinding, ParameterConfig,
10    ParameterKind, ParameterValue, ScalarType, StrategyConfig, StrategyInput, Value,
11};
12
13/// A configured strategy document that may still contain declared parameter references.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct StrategyTemplate {
17    document: StrategyConfig,
18}
19
20impl StrategyTemplate {
21    pub fn new(document: StrategyConfig) -> Self {
22        Self { document }
23    }
24
25    pub fn document(&self) -> &StrategyConfig {
26        &self.document
27    }
28
29    pub fn into_document(self) -> StrategyConfig {
30        self.document
31    }
32
33    /// Validate template shape and compile a representative binding for every choice option.
34    pub fn validate(&self, library: &MaterialLibrary) -> Result<(), CompileError> {
35        let declarations = validate_parameter_declarations(&self.document.parameters)?;
36        validate_template_references(&self.document, &declarations, library)?;
37
38        let baseline = representative_binding(&self.document.parameters)?;
39        self.bind_internal(&baseline, library, true)?;
40        for parameter in &self.document.parameters {
41            if let ParameterKind::Choice { options } = &parameter.kind {
42                for option in options {
43                    let mut binding = baseline.clone();
44                    binding
45                        .0
46                        .insert(parameter.id.clone(), ParameterValue::Choice(option.clone()));
47                    self.bind_internal(&binding, library, true)?;
48                }
49            }
50        }
51        Ok(())
52    }
53
54    /// Substitute one complete parameter binding and return an ordinary compilable document.
55    pub fn bind(
56        &self,
57        binding: &ParameterBinding,
58        library: &MaterialLibrary,
59    ) -> Result<StrategyConfig, CompileError> {
60        let declarations = validate_parameter_declarations(&self.document.parameters)?;
61        validate_template_references(&self.document, &declarations, library)?;
62        validate_binding(&self.document.parameters, binding)?;
63        self.bind_internal(binding, library, false)
64    }
65
66    fn bind_internal(
67        &self,
68        binding: &ParameterBinding,
69        library: &MaterialLibrary,
70        compile: bool,
71    ) -> Result<StrategyConfig, CompileError> {
72        validate_binding(&self.document.parameters, binding)?;
73        let declarations: BTreeMap<_, _> = self
74            .document
75            .parameters
76            .iter()
77            .map(|parameter| (parameter.id.as_str(), parameter))
78            .collect();
79        let mut document = self.document.clone();
80        document.parameters.clear();
81        for (index, material) in document.materials.iter_mut().enumerate() {
82            material.inputs = material
83                .inputs
84                .iter()
85                .map(|expr| bind_expr(expr, binding, &format!("materials[{index}].inputs")))
86                .collect::<Result<_, _>>()?;
87            let schema = library.parameter_schema(&material.key).ok_or_else(|| {
88                CompileError::UnknownMaterialKey {
89                    path: format!("materials[{index}].key"),
90                    key: material.key.clone(),
91                }
92            })?;
93            material.params = bind_material_args(
94                &material.params,
95                schema,
96                &declarations,
97                binding,
98                &format!("materials[{index}].params"),
99            )?;
100        }
101        for (state_index, state) in document.states.iter_mut().enumerate() {
102            for (transition_index, transition) in state.transitions.iter_mut().enumerate() {
103                let path = format!("states[{state_index}].transitions[{transition_index}]");
104                transition.when = bind_expr(&transition.when, binding, &format!("{path}.when"))?;
105                for (index, assignment) in transition.assignments.iter_mut().enumerate() {
106                    assignment.value = bind_expr(
107                        &assignment.value,
108                        binding,
109                        &format!("{path}.assignments[{index}].value"),
110                    )?;
111                }
112                if let Some(decision) = transition.decision.as_mut() {
113                    for (index, value) in decision.values.iter_mut().enumerate() {
114                        value.value = bind_expr(
115                            &value.value,
116                            binding,
117                            &format!("{path}.decision.values[{index}]"),
118                        )?;
119                    }
120                }
121                for (index, action) in transition.actions.iter_mut().enumerate() {
122                    bind_action(action, binding, &format!("{path}.actions[{index}]"))?;
123                }
124                for (note_index, note) in transition.notes.iter_mut().enumerate() {
125                    for (value_index, value) in note.values.iter_mut().enumerate() {
126                        value.value = bind_expr(
127                            &value.value,
128                            binding,
129                            &format!("{path}.notes[{note_index}].values[{value_index}]"),
130                        )?;
131                    }
132                }
133            }
134        }
135        prune_materials(&mut document)?;
136        if compile {
137            ConfiguredStrategy::compile(document.clone(), library, "template", "TEMPLATE")?;
138        }
139        Ok(document)
140    }
141}
142
143impl From<StrategyConfig> for StrategyTemplate {
144    fn from(value: StrategyConfig) -> Self {
145        Self::new(value)
146    }
147}
148
149fn validate_parameter_declarations(
150    parameters: &[ParameterConfig],
151) -> Result<BTreeMap<&str, &ParameterConfig>, CompileError> {
152    if parameters.len() > crate::MAX_PARAMETERS {
153        return Err(CompileError::ExcessiveBound {
154            path: "parameters".into(),
155            actual: parameters.len(),
156            limit: crate::MAX_PARAMETERS,
157        });
158    }
159    let mut declarations = BTreeMap::new();
160    for (index, parameter) in parameters.iter().enumerate() {
161        crate::validate_id(&parameter.id).map_err(|reason| CompileError::InvalidIdentifier {
162            path: format!("parameters[{index}].id"),
163            reason,
164        })?;
165        if declarations
166            .insert(parameter.id.as_str(), parameter)
167            .is_some()
168        {
169            return Err(CompileError::DuplicateIdentifier {
170                path: "parameters".into(),
171                id: parameter.id.clone(),
172            });
173        }
174        if let ParameterKind::Choice { options } = &parameter.kind {
175            if options.is_empty() || options.len() > crate::MAX_PARAMETER_OPTIONS {
176                return Err(CompileError::InvalidConfig {
177                    path: format!("parameters[{index}].options"),
178                    reason: "choice options must be non-empty and bounded".into(),
179                });
180            }
181            let mut seen = BTreeSet::new();
182            for option in options {
183                crate::validate_id(option).map_err(|reason| CompileError::InvalidIdentifier {
184                    path: format!("parameters[{index}].options"),
185                    reason,
186                })?;
187                if !seen.insert(option) {
188                    return Err(CompileError::DuplicateIdentifier {
189                        path: format!("parameters[{index}].options"),
190                        id: option.clone(),
191                    });
192                }
193            }
194        }
195    }
196    Ok(declarations)
197}
198
199fn validate_template_references(
200    document: &StrategyConfig,
201    declarations: &BTreeMap<&str, &ParameterConfig>,
202    library: &MaterialLibrary,
203) -> Result<(), CompileError> {
204    for (index, material) in document.materials.iter().enumerate() {
205        let schema = library.parameter_schema(&material.key).ok_or_else(|| {
206            CompileError::UnknownMaterialKey {
207                path: format!("materials[{index}].key"),
208                key: material.key.clone(),
209            }
210        })?;
211        validate_material_template_args(
212            &material.params,
213            schema,
214            declarations,
215            &format!("materials[{index}].params"),
216        )?;
217        for (input_index, input) in material.inputs.iter().enumerate() {
218            validate_expr_template(
219                input,
220                declarations,
221                &format!("materials[{index}].inputs[{input_index}]"),
222            )?;
223        }
224    }
225    for (state_index, state) in document.states.iter().enumerate() {
226        for (transition_index, transition) in state.transitions.iter().enumerate() {
227            let path = format!("states[{state_index}].transitions[{transition_index}]");
228            validate_expr_template(&transition.when, declarations, &format!("{path}.when"))?;
229            for (index, assignment) in transition.assignments.iter().enumerate() {
230                validate_expr_template(
231                    &assignment.value,
232                    declarations,
233                    &format!("{path}.assignments[{index}].value"),
234                )?;
235            }
236            if let Some(decision) = &transition.decision {
237                for (index, value) in decision.values.iter().enumerate() {
238                    validate_expr_template(
239                        &value.value,
240                        declarations,
241                        &format!("{path}.decision.values[{index}]"),
242                    )?;
243                }
244            }
245            for (index, action) in transition.actions.iter().enumerate() {
246                visit_action_exprs(action, |expr, suffix| {
247                    validate_expr_template(
248                        expr,
249                        declarations,
250                        &format!("{path}.actions[{index}].{suffix}"),
251                    )
252                })?;
253            }
254            for (note_index, note) in transition.notes.iter().enumerate() {
255                for (value_index, value) in note.values.iter().enumerate() {
256                    validate_expr_template(
257                        &value.value,
258                        declarations,
259                        &format!("{path}.notes[{note_index}].values[{value_index}]"),
260                    )?;
261                }
262            }
263        }
264    }
265    Ok(())
266}
267
268fn validate_expr_template(
269    expr: &Expr,
270    declarations: &BTreeMap<&str, &ParameterConfig>,
271    path: &str,
272) -> Result<(), CompileError> {
273    match expr {
274        Expr::Param { id } => {
275            if !declarations.contains_key(id.as_str()) {
276                return Err(CompileError::UnknownReference {
277                    path: path.into(),
278                    reference: id.clone(),
279                });
280            }
281        }
282        Expr::Select { param, cases } => {
283            let declaration =
284                declarations
285                    .get(param.as_str())
286                    .ok_or_else(|| CompileError::UnknownReference {
287                        path: format!("{path}.param"),
288                        reference: param.clone(),
289                    })?;
290            let ParameterKind::Choice { options } = &declaration.kind else {
291                return Err(CompileError::InvalidConfig {
292                    path: format!("{path}.param"),
293                    reason: "select requires a choice parameter".into(),
294                });
295            };
296            let expected: BTreeSet<_> = options.iter().map(String::as_str).collect();
297            let actual: BTreeSet<_> = cases.keys().map(String::as_str).collect();
298            if expected != actual {
299                return Err(CompileError::InvalidConfig {
300                    path: format!("{path}.cases"),
301                    reason: "select cases must exactly match the declared choice options".into(),
302                });
303            }
304            let mut known_types = cases
305                .values()
306                .filter_map(|case| template_type_hint(case, declarations));
307            let first_type = known_types.next();
308            if first_type.is_some_and(|first| known_types.any(|value| value != first)) {
309                return Err(CompileError::InvalidConfig {
310                    path: format!("{path}.cases"),
311                    reason: "select cases must have the same expression type".into(),
312                });
313            }
314            for (name, case) in cases {
315                validate_expr_template(case, declarations, &format!("{path}.cases.{name}"))?;
316            }
317        }
318        Expr::Not { value }
319        | Expr::Strict { value }
320        | Expr::Abs { value }
321        | Expr::IsPresent { value }
322        | Expr::IsMissing { value } => validate_expr_template(value, declarations, path)?,
323        Expr::Eq { left, right }
324        | Expr::Ne { left, right }
325        | Expr::Lt { left, right }
326        | Expr::Le { left, right }
327        | Expr::Gt { left, right }
328        | Expr::Ge { left, right }
329        | Expr::Add { left, right }
330        | Expr::Sub { left, right }
331        | Expr::Mul { left, right }
332        | Expr::Div { left, right }
333        | Expr::Min { left, right }
334        | Expr::Max { left, right } => {
335            validate_expr_template(left, declarations, path)?;
336            validate_expr_template(right, declarations, path)?;
337        }
338        Expr::All { items } | Expr::Any { items } => {
339            for item in items {
340                validate_expr_template(item, declarations, path)?;
341            }
342        }
343        _ => {}
344    }
345    Ok(())
346}
347
348fn template_type_hint(
349    expr: &Expr,
350    declarations: &BTreeMap<&str, &ParameterConfig>,
351) -> Option<crate::ValueType> {
352    use crate::{ScalarType, ValueType};
353    match expr {
354        Expr::Literal { value } => Some(value.value_type()),
355        Expr::Param { id } => declarations
356            .get(id.as_str())
357            .map(|parameter| ValueType::required(parameter_scalar_type(&parameter.kind))),
358        Expr::Input { value_type, .. } => Some(*value_type),
359        Expr::Bar { field, .. } => Some(crate::bar_field_type(*field)),
360        Expr::Position { field, .. } => Some(crate::material::position_field_type(*field)),
361        Expr::Feedback { .. } | Expr::Readiness => Some(ValueType::required(ScalarType::Bool)),
362        Expr::InputTime => Some(ValueType::required(ScalarType::Timestamp)),
363        Expr::Eq { .. }
364        | Expr::Ne { .. }
365        | Expr::Lt { .. }
366        | Expr::Le { .. }
367        | Expr::Gt { .. }
368        | Expr::Ge { .. }
369        | Expr::All { .. }
370        | Expr::Any { .. }
371        | Expr::Not { .. }
372        | Expr::IsPresent { .. }
373        | Expr::IsMissing { .. } => Some(ValueType::required(ScalarType::Bool)),
374        Expr::Strict { .. } => Some(ValueType::optional(ScalarType::Bool)),
375        Expr::Abs { value } => template_type_hint(value, declarations),
376        Expr::Min { left, right } | Expr::Max { left, right } => {
377            let left = template_type_hint(left, declarations)?;
378            let right = template_type_hint(right, declarations)?;
379            (left.scalar == right.scalar).then_some(ValueType {
380                scalar: left.scalar,
381                optional: left.optional || right.optional,
382            })
383        }
384        Expr::Add { left, right }
385        | Expr::Sub { left, right }
386        | Expr::Mul { left, right }
387        | Expr::Div { left, right } => {
388            let left = template_type_hint(left, declarations)?;
389            let right = template_type_hint(right, declarations)?;
390            let scalar = template_arithmetic_type(expr, left.scalar, right.scalar)?;
391            Some(ValueType {
392                scalar,
393                optional: left.optional || right.optional,
394            })
395        }
396        Expr::Select { cases, .. } => {
397            let mut values = cases
398                .values()
399                .filter_map(|case| template_type_hint(case, declarations));
400            let first = values.next()?;
401            values.all(|value| value == first).then_some(first)
402        }
403        Expr::Material { .. } | Expr::Variable { .. } => None,
404    }
405}
406
407fn template_arithmetic_type(
408    expr: &Expr,
409    left: ScalarType,
410    right: ScalarType,
411) -> Option<ScalarType> {
412    use ScalarType::*;
413    let add = matches!(expr, Expr::Add { .. });
414    let sub = matches!(expr, Expr::Sub { .. });
415    let mul = matches!(expr, Expr::Mul { .. });
416    let div = matches!(expr, Expr::Div { .. });
417    match (left, right) {
418        (Integer, Integer) if add || sub || mul || div => Some(Integer),
419        (Number, Number) if add || sub || mul || div => Some(Number),
420        (Price, Price) if add || sub => Some(Price),
421        (Price, Number) if mul || div => Some(Price),
422        (Number, Price) if mul => Some(Price),
423        (Price, Price) if div => Some(Number),
424        (Duration, Duration) if add || sub => Some(Duration),
425        (Timestamp, Duration) if add || sub => Some(Timestamp),
426        (Duration, Timestamp) if add => Some(Timestamp),
427        (Timestamp, Timestamp) if sub => Some(Duration),
428        _ => None,
429    }
430}
431
432fn validate_material_template_args(
433    args: &MaterialArgs,
434    schema: &[crate::ParamSpec],
435    declarations: &BTreeMap<&str, &ParameterConfig>,
436    path: &str,
437) -> Result<(), CompileError> {
438    if args.len() > crate::MAX_MATERIAL_ARGS {
439        return Err(CompileError::ExcessiveBound {
440            path: path.into(),
441            actual: args.len(),
442            limit: crate::MAX_MATERIAL_ARGS,
443        });
444    }
445    for (name, arg) in args.iter() {
446        let spec = schema
447            .iter()
448            .find(|spec| spec.name == name)
449            .ok_or_else(|| CompileError::InvalidConfig {
450                path: format!("{path}.{name}"),
451                reason: "material argument is not declared by the factory".into(),
452            })?;
453        if let MaterialArg::Param(id) = arg {
454            let declaration =
455                declarations
456                    .get(id.as_str())
457                    .ok_or_else(|| CompileError::UnknownReference {
458                        path: format!("{path}.{name}"),
459                        reference: id.clone(),
460                    })?;
461            if !parameter_matches_material_kind(&declaration.kind, spec.kind) {
462                return Err(CompileError::InvalidConfig {
463                    path: format!("{path}.{name}"),
464                    reason: "parameter kind does not match the material factory schema".into(),
465                });
466            }
467        } else if !literal_matches_material_kind(arg, spec.kind) {
468            return Err(CompileError::InvalidConfig {
469                path: format!("{path}.{name}"),
470                reason: "material argument does not match the material factory schema".into(),
471            });
472        }
473    }
474    for spec in schema {
475        if spec.required && args.get(spec.name).is_none() {
476            return Err(CompileError::InvalidConfig {
477                path: format!("{path}.{}", spec.name),
478                reason: "required material argument is missing".into(),
479            });
480        }
481    }
482    Ok(())
483}
484
485fn parameter_matches_material_kind(parameter: &ParameterKind, material: ParamKind) -> bool {
486    matches!(
487        (parameter, material),
488        (ParameterKind::Integer, ParamKind::Integer { .. })
489            | (ParameterKind::Number, ParamKind::Number { .. })
490            | (
491                ParameterKind::Choice { .. },
492                ParamKind::Source | ParamKind::Slot | ParamKind::BarField | ParamKind::ActionKind
493            )
494    )
495}
496
497fn literal_matches_material_kind(arg: &MaterialArg, kind: ParamKind) -> bool {
498    match (arg, kind) {
499        (MaterialArg::Integer(value), ParamKind::Integer { min, max }) => {
500            *value >= min && *value <= max
501        }
502        (MaterialArg::Number(value), ParamKind::Number { min, max }) => {
503            value.is_finite() && *value >= min && *value <= max
504        }
505        (MaterialArg::Source(_), ParamKind::Source)
506        | (MaterialArg::Slot(_), ParamKind::Slot)
507        | (MaterialArg::BarField(_), ParamKind::BarField)
508        | (MaterialArg::ActionKind(_), ParamKind::ActionKind) => true,
509        _ => false,
510    }
511}
512
513fn representative_binding(
514    parameters: &[ParameterConfig],
515) -> Result<ParameterBinding, CompileError> {
516    let mut values = BTreeMap::new();
517    for parameter in parameters {
518        let value = match &parameter.kind {
519            ParameterKind::Integer => ParameterValue::Integer(1),
520            ParameterKind::Number => ParameterValue::Number(1.0),
521            ParameterKind::Choice { options } => ParameterValue::Choice(
522                options
523                    .first()
524                    .ok_or_else(|| CompileError::InvalidConfig {
525                        path: format!("parameters.{}", parameter.id),
526                        reason: "choice parameter has no options".into(),
527                    })?
528                    .clone(),
529            ),
530        };
531        values.insert(parameter.id.clone(), value);
532    }
533    Ok(ParameterBinding(values))
534}
535
536fn validate_binding(
537    parameters: &[ParameterConfig],
538    binding: &ParameterBinding,
539) -> Result<(), CompileError> {
540    if parameters.len() != binding.0.len() {
541        return Err(CompileError::InvalidConfig {
542            path: "binding".into(),
543            reason: "every declared parameter must be bound exactly once".into(),
544        });
545    }
546    for parameter in parameters {
547        let value = binding
548            .get(&parameter.id)
549            .ok_or_else(|| CompileError::InvalidConfig {
550                path: format!("binding.{}", parameter.id),
551                reason: "declared parameter is not bound".into(),
552            })?;
553        let valid = match (&parameter.kind, value) {
554            (ParameterKind::Integer, ParameterValue::Integer(_)) => true,
555            (ParameterKind::Number, ParameterValue::Number(value)) => value.is_finite(),
556            (ParameterKind::Choice { options }, ParameterValue::Choice(value)) => {
557                options.contains(value)
558            }
559            _ => false,
560        };
561        if !valid {
562            return Err(CompileError::InvalidConfig {
563                path: format!("binding.{}", parameter.id),
564                reason: "bound value does not match the parameter declaration".into(),
565            });
566        }
567    }
568    for name in binding.0.keys() {
569        if !parameters.iter().any(|parameter| parameter.id == *name) {
570            return Err(CompileError::UnknownReference {
571                path: "binding".into(),
572                reference: name.clone(),
573            });
574        }
575    }
576    Ok(())
577}
578
579fn bind_material_args(
580    args: &MaterialArgs,
581    schema: &[crate::ParamSpec],
582    declarations: &BTreeMap<&str, &ParameterConfig>,
583    binding: &ParameterBinding,
584    path: &str,
585) -> Result<MaterialArgs, CompileError> {
586    let mut bound = BTreeMap::new();
587    for (name, arg) in args.iter() {
588        let value = match arg {
589            MaterialArg::Param(id) => {
590                let declaration = declarations[id.as_str()];
591                let spec = schema
592                    .iter()
593                    .find(|spec| spec.name == name)
594                    .ok_or_else(|| CompileError::InvalidConfig {
595                        path: format!("{path}.{name}"),
596                        reason: "material argument is not declared by the factory".into(),
597                    })?;
598                bind_material_parameter(
599                    binding
600                        .get(id)
601                        .ok_or_else(|| CompileError::UnknownReference {
602                            path: format!("{path}.{name}"),
603                            reference: id.clone(),
604                        })?,
605                    &declaration.kind,
606                    spec.kind,
607                    &format!("{path}.{name}"),
608                )?
609            }
610            value => value.clone(),
611        };
612        bound.insert(name.clone(), value);
613    }
614    Ok(MaterialArgs(bound))
615}
616
617fn bind_material_parameter(
618    value: &ParameterValue,
619    declaration: &ParameterKind,
620    kind: ParamKind,
621    path: &str,
622) -> Result<MaterialArg, CompileError> {
623    let invalid = || CompileError::InvalidConfig {
624        path: path.into(),
625        reason: "bound parameter cannot be converted to the material argument kind".into(),
626    };
627    match (declaration, value, kind) {
628        (ParameterKind::Integer, ParameterValue::Integer(value), ParamKind::Integer { .. }) => {
629            Ok(MaterialArg::Integer(*value))
630        }
631        (ParameterKind::Number, ParameterValue::Number(value), ParamKind::Number { .. }) => {
632            Ok(MaterialArg::Number(*value))
633        }
634        (ParameterKind::Choice { .. }, ParameterValue::Choice(value), ParamKind::Source) => {
635            SourceIdFromChoice::source(value)
636                .map(MaterialArg::Source)
637                .map_err(|_| invalid())
638        }
639        (ParameterKind::Choice { .. }, ParameterValue::Choice(value), ParamKind::Slot) => {
640            crate::validate_id(value).map_err(|_| invalid())?;
641            Ok(MaterialArg::Slot(value.clone()))
642        }
643        (ParameterKind::Choice { .. }, ParameterValue::Choice(value), ParamKind::BarField) => {
644            parse_bar_field(value)
645                .map(MaterialArg::BarField)
646                .ok_or_else(invalid)
647        }
648        (ParameterKind::Choice { .. }, ParameterValue::Choice(value), ParamKind::ActionKind) => {
649            parse_action_kind(value)
650                .map(MaterialArg::ActionKind)
651                .ok_or_else(invalid)
652        }
653        _ => Err(invalid()),
654    }
655}
656
657struct SourceIdFromChoice;
658impl SourceIdFromChoice {
659    fn source(value: &str) -> Result<crate::SourceId, String> {
660        crate::SourceId::new(value)
661    }
662}
663
664fn parse_bar_field(value: &str) -> Option<BarField> {
665    match value {
666        "open" => Some(BarField::Open),
667        "high" => Some(BarField::High),
668        "low" => Some(BarField::Low),
669        "close" => Some(BarField::Close),
670        "volume" => Some(BarField::Volume),
671        _ => None,
672    }
673}
674
675fn parse_action_kind(value: &str) -> Option<ConfiguredActionKind> {
676    match value {
677        "entry" => Some(ConfiguredActionKind::Entry),
678        "close" => Some(ConfiguredActionKind::Close),
679        "close_partial" => Some(ConfiguredActionKind::ClosePartial),
680        "move_stoploss_to_entry" => Some(ConfiguredActionKind::MoveStoplossToEntry),
681        "modify_stoploss" => Some(ConfiguredActionKind::ModifyStoploss),
682        "cancel_pending" => Some(ConfiguredActionKind::CancelPending),
683        _ => None,
684    }
685}
686
687pub fn bind_expr(
688    expr: &Expr,
689    binding: &ParameterBinding,
690    path: &str,
691) -> Result<Expr, CompileError> {
692    let child = |value: &Expr, suffix: &str| bind_expr(value, binding, &format!("{path}.{suffix}"));
693    Ok(match expr {
694        Expr::Param { id } => Expr::Literal {
695            value: parameter_literal(binding.get(id).ok_or_else(|| {
696                CompileError::UnknownReference {
697                    path: path.into(),
698                    reference: id.clone(),
699                }
700            })?),
701        },
702        Expr::Select { param, cases } => {
703            let ParameterValue::Choice(selected) =
704                binding
705                    .get(param)
706                    .ok_or_else(|| CompileError::UnknownReference {
707                        path: format!("{path}.param"),
708                        reference: param.clone(),
709                    })?
710            else {
711                return Err(CompileError::InvalidConfig {
712                    path: format!("{path}.param"),
713                    reason: "select requires a bound choice parameter".into(),
714                });
715            };
716            let case = cases
717                .get(selected)
718                .ok_or_else(|| CompileError::InvalidConfig {
719                    path: format!("{path}.cases"),
720                    reason: format!("select has no case for '{selected}'"),
721                })?;
722            bind_expr(case, binding, &format!("{path}.cases.{selected}"))?
723        }
724        Expr::Not { value } => Expr::Not {
725            value: Box::new(child(value, "value")?),
726        },
727        Expr::Strict { value } => Expr::Strict {
728            value: Box::new(child(value, "value")?),
729        },
730        Expr::Abs { value } => Expr::Abs {
731            value: Box::new(child(value, "value")?),
732        },
733        Expr::IsPresent { value } => Expr::IsPresent {
734            value: Box::new(child(value, "value")?),
735        },
736        Expr::IsMissing { value } => Expr::IsMissing {
737            value: Box::new(child(value, "value")?),
738        },
739        Expr::Eq { left, right } => {
740            binary(|left, right| Expr::Eq { left, right }, left, right, &child)?
741        }
742        Expr::Ne { left, right } => {
743            binary(|left, right| Expr::Ne { left, right }, left, right, &child)?
744        }
745        Expr::Lt { left, right } => {
746            binary(|left, right| Expr::Lt { left, right }, left, right, &child)?
747        }
748        Expr::Le { left, right } => {
749            binary(|left, right| Expr::Le { left, right }, left, right, &child)?
750        }
751        Expr::Gt { left, right } => {
752            binary(|left, right| Expr::Gt { left, right }, left, right, &child)?
753        }
754        Expr::Ge { left, right } => {
755            binary(|left, right| Expr::Ge { left, right }, left, right, &child)?
756        }
757        Expr::Add { left, right } => {
758            binary(|left, right| Expr::Add { left, right }, left, right, &child)?
759        }
760        Expr::Sub { left, right } => {
761            binary(|left, right| Expr::Sub { left, right }, left, right, &child)?
762        }
763        Expr::Mul { left, right } => {
764            binary(|left, right| Expr::Mul { left, right }, left, right, &child)?
765        }
766        Expr::Div { left, right } => {
767            binary(|left, right| Expr::Div { left, right }, left, right, &child)?
768        }
769        Expr::Min { left, right } => {
770            binary(|left, right| Expr::Min { left, right }, left, right, &child)?
771        }
772        Expr::Max { left, right } => {
773            binary(|left, right| Expr::Max { left, right }, left, right, &child)?
774        }
775        Expr::All { items } => Expr::All {
776            items: items
777                .iter()
778                .enumerate()
779                .map(|(index, item)| child(item, &format!("items[{index}]")))
780                .collect::<Result<_, _>>()?,
781        },
782        Expr::Any { items } => Expr::Any {
783            items: items
784                .iter()
785                .enumerate()
786                .map(|(index, item)| child(item, &format!("items[{index}]")))
787                .collect::<Result<_, _>>()?,
788        },
789        value => value.clone(),
790    })
791}
792
793fn binary<F>(
794    constructor: F,
795    left: &Expr,
796    right: &Expr,
797    child: &impl Fn(&Expr, &str) -> Result<Expr, CompileError>,
798) -> Result<Expr, CompileError>
799where
800    F: FnOnce(Box<Expr>, Box<Expr>) -> Expr,
801{
802    Ok(constructor(
803        Box::new(child(left, "left")?),
804        Box::new(child(right, "right")?),
805    ))
806}
807
808fn parameter_literal(value: &ParameterValue) -> crate::Literal {
809    match value {
810        ParameterValue::Integer(value) => crate::Literal::Integer(*value),
811        ParameterValue::Number(value) => crate::Literal::Number(*value),
812        ParameterValue::Choice(value) => crate::Literal::Text(value.clone()),
813    }
814}
815
816fn bind_action(
817    action: &mut ActionTemplate,
818    binding: &ParameterBinding,
819    path: &str,
820) -> Result<(), CompileError> {
821    visit_action_exprs_mut(action, |expr, suffix| {
822        *expr = bind_expr(expr, binding, &format!("{path}.{suffix}"))?;
823        Ok(())
824    })
825}
826
827fn visit_action_exprs(
828    action: &ActionTemplate,
829    mut visitor: impl FnMut(&Expr, &str) -> Result<(), CompileError>,
830) -> Result<(), CompileError> {
831    match action {
832        ActionTemplate::Entry {
833            side,
834            price,
835            risk,
836            stoploss,
837            targets,
838            ..
839        } => {
840            visitor(side, "side")?;
841            visitor(price, "price")?;
842            visitor(risk, "risk")?;
843            visitor(stoploss, "stoploss")?;
844            for (index, target) in targets.iter().enumerate() {
845                visitor(target, &format!("targets[{index}]"))?;
846            }
847        }
848        ActionTemplate::ClosePartial { ratio, .. } => visitor(ratio, "ratio")?,
849        ActionTemplate::ModifyStoploss { price, .. } => visitor(price, "price")?,
850        _ => {}
851    }
852    Ok(())
853}
854
855fn visit_action_exprs_mut(
856    action: &mut ActionTemplate,
857    mut visitor: impl FnMut(&mut Expr, &str) -> Result<(), CompileError>,
858) -> Result<(), CompileError> {
859    match action {
860        ActionTemplate::Entry {
861            side,
862            price,
863            risk,
864            stoploss,
865            targets,
866            ..
867        } => {
868            visitor(side, "side")?;
869            visitor(price, "price")?;
870            visitor(risk, "risk")?;
871            visitor(stoploss, "stoploss")?;
872            for (index, target) in targets.iter_mut().enumerate() {
873                visitor(target, &format!("targets[{index}]"))?;
874            }
875        }
876        ActionTemplate::ClosePartial { ratio, .. } => visitor(ratio, "ratio")?,
877        ActionTemplate::ModifyStoploss { price, .. } => visitor(price, "price")?,
878        _ => {}
879    }
880    Ok(())
881}
882
883fn prune_materials(document: &mut StrategyConfig) -> Result<(), CompileError> {
884    let state_by_id: BTreeMap<_, _> = document
885        .states
886        .iter()
887        .enumerate()
888        .map(|(index, state)| (state.id.as_str(), index))
889        .collect();
890    let initial = state_by_id
891        .get(document.initial_state.as_str())
892        .copied()
893        .ok_or_else(|| CompileError::InvalidStateTarget {
894            path: "initial_state".into(),
895            target: document.initial_state.clone(),
896        })?;
897    let mut reachable_states = BTreeSet::from([initial]);
898    let mut pending = vec![initial];
899    while let Some(index) = pending.pop() {
900        for transition in &document.states[index].transitions {
901            let target = state_by_id
902                .get(transition.target.as_str())
903                .copied()
904                .ok_or_else(|| CompileError::InvalidStateTarget {
905                    path: format!("states[{index}].transitions.target"),
906                    target: transition.target.clone(),
907                })?;
908            if reachable_states.insert(target) {
909                pending.push(target);
910            }
911        }
912    }
913
914    let mut roots = Vec::new();
915    for state_index in reachable_states {
916        let state = &document.states[state_index];
917        for (transition_index, transition) in state.transitions.iter().enumerate() {
918            let path = format!("states[{state_index}].transitions[{transition_index}]");
919            collect_material_refs(&transition.when, &mut roots, &format!("{path}.when"))?;
920            for assignment in &transition.assignments {
921                collect_material_refs(&assignment.value, &mut roots, &path)?;
922            }
923            if let Some(decision) = &transition.decision {
924                for value in &decision.values {
925                    collect_material_refs(&value.value, &mut roots, &path)?;
926                }
927            }
928            for action in &transition.actions {
929                visit_action_exprs(action, |expr, suffix| {
930                    collect_material_refs(expr, &mut roots, &format!("{path}.{suffix}"))
931                })?;
932            }
933            for note in &transition.notes {
934                for value in &note.values {
935                    collect_material_refs(&value.value, &mut roots, &path)?;
936                }
937            }
938        }
939    }
940
941    let materials: BTreeMap<_, _> = document
942        .materials
943        .iter()
944        .map(|material| (material.id.as_str(), material))
945        .collect();
946    let mut reachable = BTreeSet::new();
947    let mut pending = roots;
948    while let Some(id) = pending.pop() {
949        if !reachable.insert(id.clone()) {
950            continue;
951        }
952        let material =
953            materials
954                .get(id.as_str())
955                .ok_or_else(|| CompileError::UnknownReference {
956                    path: "materials".into(),
957                    reference: id,
958                })?;
959        for input in &material.inputs {
960            collect_material_refs(input, &mut pending, "materials.inputs")?;
961        }
962    }
963    document
964        .materials
965        .retain(|material| reachable.contains(&material.id));
966    Ok(())
967}
968
969/// Evaluate a parameter-and-literal-only expression after substituting a binding.
970pub fn evaluate_parameter_expression(
971    expr: &Expr,
972    binding: &ParameterBinding,
973) -> Result<Value, CompileError> {
974    let bound = bind_expr(expr, binding, "constraint")?;
975    let variables = BTreeMap::new();
976    let materials = BTreeMap::new();
977    let trade_slots = BTreeSet::new();
978    let sources = BTreeSet::new();
979    let scope = ExprScope {
980        variables: &variables,
981        materials: &materials,
982        trade_slots: &trade_slots,
983        sources: &sources,
984    };
985    let (compiled, _) = compile_expr(&bound, &scope, "constraint")?;
986    let input = StrategyInput {
987        time: NaiveDateTime::default(),
988        ready: true,
989        completed_bars: Vec::new(),
990        values: Vec::new(),
991        trade_slots: Vec::new(),
992        feedback: Vec::new(),
993    };
994    compiled
995        .eval(
996            &EvalScope {
997                variables: &[],
998                materials: &[],
999                input: &input,
1000                feedback: &[],
1001            },
1002            "constraint",
1003        )
1004        .map_err(|error| CompileError::InvalidConfig {
1005            path: "constraint".into(),
1006            reason: error.to_string(),
1007        })
1008}
1009
1010pub fn parameter_value_label(value: &ParameterValue) -> String {
1011    match value {
1012        ParameterValue::Integer(value) => value.to_string(),
1013        ParameterValue::Number(value) => value.to_string(),
1014        ParameterValue::Choice(value) => value.clone(),
1015    }
1016}
1017
1018pub fn require_boolean(value: Value, path: &str) -> Result<bool, CompileError> {
1019    match value {
1020        Value::Bool(value) => Ok(value),
1021        value => Err(CompileError::InvalidConfig {
1022            path: path.into(),
1023            reason: format!("expected boolean result, got {:?}", value.scalar_type()),
1024        }),
1025    }
1026}
1027
1028pub fn parameter_scalar_type(kind: &ParameterKind) -> ScalarType {
1029    match kind {
1030        ParameterKind::Integer => ScalarType::Integer,
1031        ParameterKind::Number => ScalarType::Number,
1032        ParameterKind::Choice { .. } => ScalarType::Text,
1033    }
1034}