Skip to main content

sandbox_quant/
strategy_catalog.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4pub struct StrategyProfile {
5    pub label: String,
6    pub source_tag: String,
7    #[serde(default)]
8    pub symbol: String,
9    pub fast_period: usize,
10    pub slow_period: usize,
11    pub min_ticks_between_signals: u64,
12}
13
14impl StrategyProfile {
15    pub fn periods_tuple(&self) -> (usize, usize, u64) {
16        (
17            self.fast_period,
18            self.slow_period,
19            self.min_ticks_between_signals,
20        )
21    }
22}
23
24#[derive(Debug, Clone)]
25pub struct StrategyCatalog {
26    profiles: Vec<StrategyProfile>,
27    next_custom_id: u32,
28}
29
30impl StrategyCatalog {
31    pub fn new(
32        default_symbol: &str,
33        config_fast: usize,
34        config_slow: usize,
35        min_ticks_between_signals: u64,
36    ) -> Self {
37        let symbol = default_symbol.trim().to_ascii_uppercase();
38        Self {
39            profiles: vec![
40                StrategyProfile {
41                    label: "MA(Config)".to_string(),
42                    source_tag: "cfg".to_string(),
43                    symbol: symbol.clone(),
44                    fast_period: config_fast,
45                    slow_period: config_slow,
46                    min_ticks_between_signals,
47                },
48                StrategyProfile {
49                    label: "MA(Fast 5/20)".to_string(),
50                    source_tag: "fst".to_string(),
51                    symbol: symbol.clone(),
52                    fast_period: 5,
53                    slow_period: 20,
54                    min_ticks_between_signals,
55                },
56                StrategyProfile {
57                    label: "MA(Slow 20/60)".to_string(),
58                    source_tag: "slw".to_string(),
59                    symbol,
60                    fast_period: 20,
61                    slow_period: 60,
62                    min_ticks_between_signals,
63                },
64            ],
65            next_custom_id: 1,
66        }
67    }
68
69    pub fn labels(&self) -> Vec<String> {
70        self.profiles.iter().map(|p| p.label.clone()).collect()
71    }
72
73    pub fn symbols(&self) -> Vec<String> {
74        self.profiles.iter().map(|p| p.symbol.clone()).collect()
75    }
76
77    pub fn len(&self) -> usize {
78        self.profiles.len()
79    }
80
81    pub fn get(&self, index: usize) -> Option<&StrategyProfile> {
82        self.profiles.get(index)
83    }
84
85    pub fn get_by_source_tag(&self, source_tag: &str) -> Option<&StrategyProfile> {
86        self.profiles.iter().find(|p| p.source_tag == source_tag)
87    }
88
89    pub fn profiles(&self) -> &[StrategyProfile] {
90        &self.profiles
91    }
92
93    pub fn index_of_label(&self, label: &str) -> Option<usize> {
94        self.profiles.iter().position(|p| p.label == label)
95    }
96
97    pub fn from_profiles(
98        mut profiles: Vec<StrategyProfile>,
99        default_symbol: &str,
100        config_fast: usize,
101        config_slow: usize,
102        min_ticks_between_signals: u64,
103    ) -> Self {
104        if profiles.is_empty() {
105            return Self::new(
106                default_symbol,
107                config_fast,
108                config_slow,
109                min_ticks_between_signals,
110            );
111        }
112        for profile in &mut profiles {
113            if profile.symbol.trim().is_empty() {
114                profile.symbol = default_symbol.trim().to_ascii_uppercase();
115            }
116        }
117        let next_custom_id = profiles
118            .iter()
119            .filter_map(|profile| {
120                let tag = profile.source_tag.strip_prefix('c')?;
121                tag.parse::<u32>().ok()
122            })
123            .max()
124            .map(|id| id + 1)
125            .unwrap_or(1);
126        Self {
127            profiles,
128            next_custom_id,
129        }
130    }
131
132    pub fn add_custom_from_index(&mut self, base_index: usize) -> StrategyProfile {
133        let base = self
134            .profiles
135            .get(base_index)
136            .cloned()
137            .unwrap_or_else(|| self.profiles[0].clone());
138        self.new_custom_profile(
139            &base.symbol,
140            base.fast_period,
141            base.slow_period,
142            base.min_ticks_between_signals,
143        )
144    }
145
146    pub fn fork_profile(
147        &mut self,
148        index: usize,
149        symbol: &str,
150        fast_period: usize,
151        slow_period: usize,
152        min_ticks_between_signals: u64,
153    ) -> Option<StrategyProfile> {
154        self.profiles.get(index)?;
155        Some(self.new_custom_profile(
156            symbol,
157            fast_period,
158            slow_period,
159            min_ticks_between_signals,
160        ))
161    }
162
163    fn new_custom_profile(
164        &mut self,
165        symbol: &str,
166        fast_period: usize,
167        slow_period: usize,
168        min_ticks_between_signals: u64,
169    ) -> StrategyProfile {
170        let tag = format!("c{:02}", self.next_custom_id);
171        self.next_custom_id += 1;
172        let fast = fast_period.max(2);
173        let slow = slow_period.max(fast + 1);
174        let profile = StrategyProfile {
175            label: format!("MA(Custom {}/{}) [{}]", fast, slow, tag),
176            source_tag: tag,
177            symbol: symbol.trim().to_ascii_uppercase(),
178            fast_period: fast,
179            slow_period: slow,
180            min_ticks_between_signals: min_ticks_between_signals.max(1),
181        };
182        self.profiles.push(profile.clone());
183        profile
184    }
185}