1use crate::{
2 Error,
3 models::{Interval, MarketAdjustment, SessionType, pine_indicator::ScriptType},
4};
5use bon::Builder;
6use iso_currency::Currency;
7use serde::{Deserialize, Serialize};
8use std::fmt;
9use ustr::Ustr;
10
11#[derive(Debug, Clone, Deserialize, Serialize, Copy)]
43pub struct ChartOptions {
44 pub symbol: Option<Ustr>,
45 pub exchange: Option<Ustr>,
46 pub interval: Interval,
47 pub bar_count: u64,
48 pub range: Option<Range>,
49 pub replay_mode: bool,
50 pub replay_from: i64,
51 pub replay_session: Option<Ustr>,
52 pub adjustment: Option<MarketAdjustment>,
53 pub currency: Option<Currency>,
54 pub session_type: Option<SessionType>,
55 pub study_config: Option<StudyOptions>,
56}
57
58#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Copy)]
63pub enum Range {
64 FromTo(u64, u64),
65 OneDay,
66 FiveDays,
67 OneMonth,
68 ThreeMonths,
69 SixMonths,
70 YearToDate,
71 OneYear,
72 FiveYears,
73 All,
74}
75
76impl Range {
77 pub fn from_to(from: u64, to: u64) -> Self {
78 Range::FromTo(from, to)
79 }
80
81 pub fn one_day() -> Self {
82 Range::OneDay
83 }
84
85 pub fn five_days() -> Self {
86 Range::FiveDays
87 }
88
89 pub fn one_month() -> Self {
90 Range::OneMonth
91 }
92
93 pub fn three_months() -> Self {
94 Range::ThreeMonths
95 }
96
97 pub fn six_months() -> Self {
98 Range::SixMonths
99 }
100
101 pub fn year_to_date() -> Self {
102 Range::YearToDate
103 }
104
105 pub fn one_year() -> Self {
106 Range::OneYear
107 }
108
109 pub fn five_years() -> Self {
110 Range::FiveYears
111 }
112
113 pub fn all() -> Self {
114 Range::All
115 }
116}
117
118impl fmt::Display for Range {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 match self {
121 Range::FromTo(from, to) => write!(f, "r,{from}:{to}"),
122 Range::OneDay => write!(f, "1D"),
123 Range::FiveDays => write!(f, "5d"),
124 Range::OneMonth => write!(f, "1M"),
125 Range::ThreeMonths => write!(f, "3M"),
126 Range::SixMonths => write!(f, "6M"),
127 Range::YearToDate => write!(f, "YTD"),
128 Range::OneYear => write!(f, "12M"),
129 Range::FiveYears => write!(f, "60M"),
130 Range::All => write!(f, "ALL"),
131 }
132 }
133}
134
135impl From<Range> for Ustr {
136 fn from(val: Range) -> Self {
137 match val {
138 Range::FromTo(from, to) => Ustr::from(&format!("r,{from}:{to}")),
139 Range::OneDay => Ustr::from("1D"),
140 Range::FiveDays => Ustr::from("5d"),
141 Range::OneMonth => Ustr::from("1M"),
142 Range::ThreeMonths => Ustr::from("3M"),
143 Range::SixMonths => Ustr::from("6M"),
144 Range::YearToDate => Ustr::from("YTD"),
145 Range::OneYear => Ustr::from("12M"),
146 Range::FiveYears => Ustr::from("60M"),
147 Range::All => Ustr::from("ALL"),
148 }
149 }
150}
151
152#[derive(Default, Debug, Clone, Deserialize, Serialize, Builder, Copy)]
158pub struct StudyOptions {
159 pub script_id: Ustr,
160 pub script_version: Ustr,
161 pub script_type: ScriptType,
162}
163
164impl Default for ChartOptions {
165 fn default() -> Self {
166 Self::builder()
167 .build()
168 .expect("Failed to create default ChartOptions")
169 }
170}
171
172#[bon::bon]
173impl ChartOptions {
174 #[builder]
175 pub fn new(
176 instrument: Option<&str>,
177 symbol: Option<&str>,
178 exchange: Option<&str>,
179 #[builder(default = Interval::OneDay)] interval: Interval,
180 #[builder(default = 500_000)] bar_count: u64,
181 range: Option<Range>,
182 #[builder(default = false)] replay_mode: bool,
183 #[builder(default = 0)] replay_from: i64,
184 replay_session: Option<&str>,
185 adjustment: Option<MarketAdjustment>,
186 currency: Option<Currency>,
187 session_type: Option<SessionType>,
188 study_config: Option<StudyOptions>,
189 ) -> Result<Self, Error> {
190 let (validated_exchange, validated_symbol) =
191 Self::validate_instrument(instrument, symbol, exchange)?;
192
193 Ok(Self {
194 symbol: validated_symbol,
195 exchange: validated_exchange,
196 interval,
197 bar_count,
198 range,
199 replay_mode,
200 replay_from,
201 replay_session: replay_session.map(Ustr::from),
202 adjustment,
203 currency,
204 session_type,
205 study_config,
206 })
207 }
208
209 fn validate_instrument(
210 instrument: Option<&str>,
211 symbol: Option<&str>,
212 exchange: Option<&str>,
213 ) -> Result<(Option<Ustr>, Option<Ustr>), String> {
214 match (instrument, symbol, exchange) {
215 (Some(instrument), None, None) => {
217 if instrument.trim().is_empty() {
218 return Err("Instrument cannot be empty or whitespace only".to_string());
219 }
220
221 let parts: Vec<&str> = instrument.split(':').collect();
222 if parts.len() != 2 {
223 return Err("Instrument must be in format 'EXCHANGE:SYMBOL'".to_string());
224 }
225
226 let exchange_part = parts[0].trim();
227 let symbol_part = parts[1].trim();
228
229 if exchange_part.is_empty() || symbol_part.is_empty() {
230 return Err(
231 "Both exchange and symbol parts must be non-empty in instrument"
232 .to_string(),
233 );
234 }
235
236 if !Self::is_valid_identifier(exchange_part)
238 || !Self::is_valid_identifier(symbol_part)
239 {
240 return Err("Exchange and symbol must contain only alphanumeric characters, hyphens, dots, and underscores".to_string());
241 }
242
243 Ok((
244 Some(Ustr::from(exchange_part)),
245 Some(Ustr::from(symbol_part)),
246 ))
247 }
248
249 (None, Some(symbol), Some(exchange)) => {
251 let symbol = symbol.trim();
252 let exchange = exchange.trim();
253
254 if symbol.is_empty() {
255 return Err("Symbol cannot be empty or whitespace only".to_string());
256 }
257 if exchange.is_empty() {
258 return Err("Exchange cannot be empty or whitespace only".to_string());
259 }
260
261 if !Self::is_valid_identifier(symbol) || !Self::is_valid_identifier(exchange) {
262 return Err("Symbol and exchange must contain only alphanumeric characters, hyphens, dots, and underscores".to_string());
263 }
264
265 Ok((Some(Ustr::from(exchange)), Some(Ustr::from(symbol))))
266 }
267
268 (None, None, None) => {
270 Err("Either instrument OR both symbol and exchange must be provided".to_string())
271 }
272 (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
273 Err("Cannot provide instrument together with symbol or exchange".to_string())
274 }
275 (None, Some(_), None) | (None, None, Some(_)) => {
276 Err("Symbol and exchange must be provided together".to_string())
277 }
278 }
279 }
280
281 fn is_valid_identifier(s: &str) -> bool {
282 !s.is_empty()
283 && s.chars().all(|c| {
284 c.is_alphanumeric()
285 || c == '$'
286 || c == '%'
287 || c == '#'
288 || c == '*'
289 || c == '('
290 || c == ')'
291 || c == ':'
292 })
293 }
294
295 pub fn interval(mut self, interval: Interval) -> Self {
296 self.interval = interval;
297 self
298 }
299
300 pub fn bar_count(mut self, bar_count: u64) -> Self {
301 self.bar_count = bar_count;
302 self
303 }
304
305 pub fn replay_mode(mut self, replay_mode: bool) -> Self {
306 self.replay_mode = replay_mode;
307 self
308 }
309
310 pub fn replay_from(mut self, replay_from: i64) -> Self {
311 self.replay_from = replay_from;
312 self
313 }
314
315 pub fn replay_session_id(mut self, replay_session_id: &str) -> Self {
316 self.replay_session = Some(Ustr::from(replay_session_id));
317 self
318 }
319
320 pub fn range(mut self, range: Range) -> Self {
322 self.range = Some(range);
323 self
324 }
325
326 pub fn adjustment(mut self, adjustment: MarketAdjustment) -> Self {
327 self.adjustment = Some(adjustment);
328 self
329 }
330
331 pub fn currency(mut self, currency: Currency) -> Self {
332 self.currency = Some(currency);
333 self
334 }
335
336 pub fn session_type(mut self, session_type: SessionType) -> Self {
337 self.session_type = Some(session_type);
338 self
339 }
340
341 pub fn study_config(
342 mut self,
343 script_id: &str,
344 script_version: &str,
345 script_type: ScriptType,
346 ) -> Self {
347 self.study_config = Some(StudyOptions {
348 script_id: Ustr::from(script_id),
349 script_version: Ustr::from(script_version),
350 script_type,
351 });
352 self
353 }
354}