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}