finance_query/backtesting/condition/
threshold.rs1use crate::backtesting::strategy::StrategyContext;
6use crate::indicators::Indicator;
7
8use super::Condition;
9
10#[derive(Debug, Clone, Copy)]
30pub struct StopLoss {
31 pub pct: f64,
33}
34
35impl StopLoss {
36 pub fn new(pct: f64) -> Self {
42 Self { pct }
43 }
44}
45
46impl Condition for StopLoss {
47 fn evaluate(&self, ctx: &StrategyContext) -> bool {
48 if let Some(pos) = ctx.position {
49 let pnl_pct = pos.unrealized_return_pct(ctx.close()) / 100.0;
50 pnl_pct <= -self.pct
51 } else {
52 false
53 }
54 }
55
56 fn required_indicators(&self) -> Vec<(String, Indicator)> {
57 vec![]
58 }
59
60 fn description(&self) -> String {
61 format!("stop loss at {:.1}%", self.pct * 100.0)
62 }
63}
64
65#[inline]
75pub fn stop_loss(pct: f64) -> StopLoss {
76 StopLoss::new(pct)
77}
78
79#[derive(Debug, Clone, Copy)]
98pub struct TakeProfit {
99 pub pct: f64,
101}
102
103impl TakeProfit {
104 pub fn new(pct: f64) -> Self {
110 Self { pct }
111 }
112}
113
114impl Condition for TakeProfit {
115 fn evaluate(&self, ctx: &StrategyContext) -> bool {
116 if let Some(pos) = ctx.position {
117 let pnl_pct = pos.unrealized_return_pct(ctx.close()) / 100.0;
118 pnl_pct >= self.pct
119 } else {
120 false
121 }
122 }
123
124 fn required_indicators(&self) -> Vec<(String, Indicator)> {
125 vec![]
126 }
127
128 fn description(&self) -> String {
129 format!("take profit at {:.1}%", self.pct * 100.0)
130 }
131}
132
133#[inline]
143pub fn take_profit(pct: f64) -> TakeProfit {
144 TakeProfit::new(pct)
145}
146
147#[derive(Debug, Clone, Copy)]
149pub struct HasPosition;
150
151impl Condition for HasPosition {
152 fn evaluate(&self, ctx: &StrategyContext) -> bool {
153 ctx.has_position()
154 }
155
156 fn required_indicators(&self) -> Vec<(String, Indicator)> {
157 vec![]
158 }
159
160 fn description(&self) -> String {
161 "has position".to_string()
162 }
163}
164
165#[inline]
167pub fn has_position() -> HasPosition {
168 HasPosition
169}
170
171#[derive(Debug, Clone, Copy)]
173pub struct NoPosition;
174
175impl Condition for NoPosition {
176 fn evaluate(&self, ctx: &StrategyContext) -> bool {
177 !ctx.has_position()
178 }
179
180 fn required_indicators(&self) -> Vec<(String, Indicator)> {
181 vec![]
182 }
183
184 fn description(&self) -> String {
185 "no position".to_string()
186 }
187}
188
189#[inline]
191pub fn no_position() -> NoPosition {
192 NoPosition
193}
194
195#[derive(Debug, Clone, Copy)]
197pub struct IsLong;
198
199impl Condition for IsLong {
200 fn evaluate(&self, ctx: &StrategyContext) -> bool {
201 ctx.is_long()
202 }
203
204 fn required_indicators(&self) -> Vec<(String, Indicator)> {
205 vec![]
206 }
207
208 fn description(&self) -> String {
209 "is long".to_string()
210 }
211}
212
213#[inline]
215pub fn is_long() -> IsLong {
216 IsLong
217}
218
219#[derive(Debug, Clone, Copy)]
221pub struct IsShort;
222
223impl Condition for IsShort {
224 fn evaluate(&self, ctx: &StrategyContext) -> bool {
225 ctx.is_short()
226 }
227
228 fn required_indicators(&self) -> Vec<(String, Indicator)> {
229 vec![]
230 }
231
232 fn description(&self) -> String {
233 "is short".to_string()
234 }
235}
236
237#[inline]
239pub fn is_short() -> IsShort {
240 IsShort
241}
242
243#[derive(Debug, Clone, Copy)]
245pub struct InProfit;
246
247impl Condition for InProfit {
248 fn evaluate(&self, ctx: &StrategyContext) -> bool {
249 if let Some(pos) = ctx.position {
250 pos.unrealized_return_pct(ctx.close()) > 0.0
251 } else {
252 false
253 }
254 }
255
256 fn required_indicators(&self) -> Vec<(String, Indicator)> {
257 vec![]
258 }
259
260 fn description(&self) -> String {
261 "in profit".to_string()
262 }
263}
264
265#[inline]
267pub fn in_profit() -> InProfit {
268 InProfit
269}
270
271#[derive(Debug, Clone, Copy)]
273pub struct InLoss;
274
275impl Condition for InLoss {
276 fn evaluate(&self, ctx: &StrategyContext) -> bool {
277 if let Some(pos) = ctx.position {
278 pos.unrealized_return_pct(ctx.close()) < 0.0
279 } else {
280 false
281 }
282 }
283
284 fn required_indicators(&self) -> Vec<(String, Indicator)> {
285 vec![]
286 }
287
288 fn description(&self) -> String {
289 "in loss".to_string()
290 }
291}
292
293#[inline]
295pub fn in_loss() -> InLoss {
296 InLoss
297}
298
299#[derive(Debug, Clone, Copy)]
301pub struct HeldForBars {
302 pub min_bars: usize,
304}
305
306impl HeldForBars {
307 pub fn new(min_bars: usize) -> Self {
309 Self { min_bars }
310 }
311}
312
313impl Condition for HeldForBars {
314 fn evaluate(&self, ctx: &StrategyContext) -> bool {
315 if let Some(pos) = ctx.position {
316 let entry_idx = ctx
318 .candles
319 .iter()
320 .position(|c| c.timestamp >= pos.entry_timestamp)
321 .unwrap_or(0);
322 let bars_held = ctx.index.saturating_sub(entry_idx);
323 bars_held >= self.min_bars
324 } else {
325 false
326 }
327 }
328
329 fn required_indicators(&self) -> Vec<(String, Indicator)> {
330 vec![]
331 }
332
333 fn description(&self) -> String {
334 format!("held for {} bars", self.min_bars)
335 }
336}
337
338#[inline]
340pub fn held_for_bars(min_bars: usize) -> HeldForBars {
341 HeldForBars::new(min_bars)
342}
343
344#[derive(Debug, Clone, Copy)]
370pub struct TrailingStop {
371 pub trail_pct: f64,
373}
374
375impl TrailingStop {
376 pub fn new(trail_pct: f64) -> Self {
382 Self { trail_pct }
383 }
384}
385
386impl Condition for TrailingStop {
387 fn evaluate(&self, ctx: &StrategyContext) -> bool {
388 if let Some(pos) = ctx.position {
389 let entry_idx = ctx
391 .candles
392 .iter()
393 .position(|c| c.timestamp >= pos.entry_timestamp)
394 .unwrap_or(0);
395
396 let current_close = ctx.close();
398
399 match pos.side {
400 crate::backtesting::position::PositionSide::Long => {
401 let peak = ctx.candles[entry_idx..=ctx.index]
403 .iter()
404 .map(|c| c.high)
405 .fold(f64::NEG_INFINITY, f64::max);
406
407 current_close <= peak * (1.0 - self.trail_pct)
409 }
410 crate::backtesting::position::PositionSide::Short => {
411 let trough = ctx.candles[entry_idx..=ctx.index]
413 .iter()
414 .map(|c| c.low)
415 .fold(f64::INFINITY, f64::min);
416
417 current_close >= trough * (1.0 + self.trail_pct)
419 }
420 }
421 } else {
422 false
423 }
424 }
425
426 fn required_indicators(&self) -> Vec<(String, Indicator)> {
427 vec![]
428 }
429
430 fn description(&self) -> String {
431 format!("trailing stop at {:.1}%", self.trail_pct * 100.0)
432 }
433}
434
435#[inline]
449pub fn trailing_stop(trail_pct: f64) -> TrailingStop {
450 TrailingStop::new(trail_pct)
451}
452
453#[derive(Debug, Clone, Copy)]
473pub struct TrailingTakeProfit {
474 pub trail_pct: f64,
476}
477
478impl TrailingTakeProfit {
479 pub fn new(trail_pct: f64) -> Self {
485 Self { trail_pct }
486 }
487}
488
489impl Condition for TrailingTakeProfit {
490 fn evaluate(&self, ctx: &StrategyContext) -> bool {
491 if let Some(pos) = ctx.position {
492 let entry_idx = ctx
494 .candles
495 .iter()
496 .position(|c| c.timestamp >= pos.entry_timestamp)
497 .unwrap_or(0);
498
499 let peak_profit_pct = ctx.candles[entry_idx..=ctx.index]
501 .iter()
502 .map(|c| pos.unrealized_return_pct(c.close))
503 .fold(f64::NEG_INFINITY, f64::max);
504
505 let current_profit_pct = pos.unrealized_return_pct(ctx.close());
507
508 let trail_threshold = self.trail_pct * 100.0;
510
511 peak_profit_pct > 0.0 && current_profit_pct <= peak_profit_pct - trail_threshold
512 } else {
513 false
514 }
515 }
516
517 fn required_indicators(&self) -> Vec<(String, Indicator)> {
518 vec![]
519 }
520
521 fn description(&self) -> String {
522 format!("trailing take profit at {:.1}%", self.trail_pct * 100.0)
523 }
524}
525
526#[inline]
541pub fn trailing_take_profit(trail_pct: f64) -> TrailingTakeProfit {
542 TrailingTakeProfit::new(trail_pct)
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn test_stop_loss_description() {
551 let sl = stop_loss(0.05);
552 assert_eq!(sl.description(), "stop loss at 5.0%");
553 }
554
555 #[test]
556 fn test_take_profit_description() {
557 let tp = take_profit(0.10);
558 assert_eq!(tp.description(), "take profit at 10.0%");
559 }
560
561 #[test]
562 fn test_position_conditions_descriptions() {
563 assert_eq!(has_position().description(), "has position");
564 assert_eq!(no_position().description(), "no position");
565 assert_eq!(is_long().description(), "is long");
566 assert_eq!(is_short().description(), "is short");
567 assert_eq!(in_profit().description(), "in profit");
568 assert_eq!(in_loss().description(), "in loss");
569 }
570
571 #[test]
572 fn test_held_for_bars_description() {
573 let hfb = held_for_bars(5);
574 assert_eq!(hfb.description(), "held for 5 bars");
575 }
576
577 #[test]
578 fn test_trailing_stop_description() {
579 let ts = trailing_stop(0.03);
580 assert_eq!(ts.description(), "trailing stop at 3.0%");
581 }
582
583 #[test]
584 fn test_trailing_take_profit_description() {
585 let ttp = trailing_take_profit(0.02);
586 assert_eq!(ttp.description(), "trailing take profit at 2.0%");
587 }
588
589 #[test]
590 fn test_no_indicators_required() {
591 assert!(stop_loss(0.05).required_indicators().is_empty());
592 assert!(take_profit(0.10).required_indicators().is_empty());
593 assert!(has_position().required_indicators().is_empty());
594 assert!(no_position().required_indicators().is_empty());
595 assert!(trailing_stop(0.03).required_indicators().is_empty());
596 assert!(trailing_take_profit(0.02).required_indicators().is_empty());
597 }
598}