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#[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 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 } = ¶meter.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 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(¶meter.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 } = ¶meter.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(¶meter.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 ¶meter.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(¶meter.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 (¶meter.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 ¬e.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
969pub 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}