finance_query/backtesting/condition/
threshold.rs1use crate::backtesting::strategy::StrategyContext;
6use crate::indicators::Indicator;
7
8use super::Condition;
9
10#[derive(Debug, Clone, Copy)]
20pub struct StopLoss {
21 pub pct: f64,
23}
24
25impl StopLoss {
26 pub fn new(pct: f64) -> Self {
32 Self { pct }
33 }
34}
35
36impl Condition for StopLoss {
37 fn evaluate(&self, ctx: &StrategyContext) -> bool {
38 if let Some(pos) = ctx.position {
39 let pnl_pct = pos.unrealized_return_pct(ctx.close()) / 100.0;
40 pnl_pct <= -self.pct
41 } else {
42 false
43 }
44 }
45
46 fn required_indicators(&self) -> Vec<(String, Indicator)> {
47 vec![]
48 }
49
50 fn description(&self) -> String {
51 format!("stop loss at {:.1}%", self.pct * 100.0)
52 }
53}
54
55#[inline]
65pub fn stop_loss(pct: f64) -> StopLoss {
66 StopLoss::new(pct)
67}
68
69#[derive(Debug, Clone, Copy)]
79pub struct TakeProfit {
80 pub pct: f64,
82}
83
84impl TakeProfit {
85 pub fn new(pct: f64) -> Self {
91 Self { pct }
92 }
93}
94
95impl Condition for TakeProfit {
96 fn evaluate(&self, ctx: &StrategyContext) -> bool {
97 if let Some(pos) = ctx.position {
98 let pnl_pct = pos.unrealized_return_pct(ctx.close()) / 100.0;
99 pnl_pct >= self.pct
100 } else {
101 false
102 }
103 }
104
105 fn required_indicators(&self) -> Vec<(String, Indicator)> {
106 vec![]
107 }
108
109 fn description(&self) -> String {
110 format!("take profit at {:.1}%", self.pct * 100.0)
111 }
112}
113
114#[inline]
124pub fn take_profit(pct: f64) -> TakeProfit {
125 TakeProfit::new(pct)
126}
127
128#[derive(Debug, Clone, Copy)]
130pub struct HasPosition;
131
132impl Condition for HasPosition {
133 fn evaluate(&self, ctx: &StrategyContext) -> bool {
134 ctx.has_position()
135 }
136
137 fn required_indicators(&self) -> Vec<(String, Indicator)> {
138 vec![]
139 }
140
141 fn description(&self) -> String {
142 "has position".to_string()
143 }
144}
145
146#[inline]
148pub fn has_position() -> HasPosition {
149 HasPosition
150}
151
152#[derive(Debug, Clone, Copy)]
154pub struct NoPosition;
155
156impl Condition for NoPosition {
157 fn evaluate(&self, ctx: &StrategyContext) -> bool {
158 !ctx.has_position()
159 }
160
161 fn required_indicators(&self) -> Vec<(String, Indicator)> {
162 vec![]
163 }
164
165 fn description(&self) -> String {
166 "no position".to_string()
167 }
168}
169
170#[inline]
172pub fn no_position() -> NoPosition {
173 NoPosition
174}
175
176#[derive(Debug, Clone, Copy)]
178pub struct IsLong;
179
180impl Condition for IsLong {
181 fn evaluate(&self, ctx: &StrategyContext) -> bool {
182 ctx.is_long()
183 }
184
185 fn required_indicators(&self) -> Vec<(String, Indicator)> {
186 vec![]
187 }
188
189 fn description(&self) -> String {
190 "is long".to_string()
191 }
192}
193
194#[inline]
196pub fn is_long() -> IsLong {
197 IsLong
198}
199
200#[derive(Debug, Clone, Copy)]
202pub struct IsShort;
203
204impl Condition for IsShort {
205 fn evaluate(&self, ctx: &StrategyContext) -> bool {
206 ctx.is_short()
207 }
208
209 fn required_indicators(&self) -> Vec<(String, Indicator)> {
210 vec![]
211 }
212
213 fn description(&self) -> String {
214 "is short".to_string()
215 }
216}
217
218#[inline]
220pub fn is_short() -> IsShort {
221 IsShort
222}
223
224#[derive(Debug, Clone, Copy)]
226pub struct InProfit;
227
228impl Condition for InProfit {
229 fn evaluate(&self, ctx: &StrategyContext) -> bool {
230 if let Some(pos) = ctx.position {
231 pos.unrealized_return_pct(ctx.close()) > 0.0
232 } else {
233 false
234 }
235 }
236
237 fn required_indicators(&self) -> Vec<(String, Indicator)> {
238 vec![]
239 }
240
241 fn description(&self) -> String {
242 "in profit".to_string()
243 }
244}
245
246#[inline]
248pub fn in_profit() -> InProfit {
249 InProfit
250}
251
252#[derive(Debug, Clone, Copy)]
254pub struct InLoss;
255
256impl Condition for InLoss {
257 fn evaluate(&self, ctx: &StrategyContext) -> bool {
258 if let Some(pos) = ctx.position {
259 pos.unrealized_return_pct(ctx.close()) < 0.0
260 } else {
261 false
262 }
263 }
264
265 fn required_indicators(&self) -> Vec<(String, Indicator)> {
266 vec![]
267 }
268
269 fn description(&self) -> String {
270 "in loss".to_string()
271 }
272}
273
274#[inline]
276pub fn in_loss() -> InLoss {
277 InLoss
278}
279
280#[derive(Debug, Clone, Copy)]
282pub struct HeldForBars {
283 pub min_bars: usize,
285}
286
287impl HeldForBars {
288 pub fn new(min_bars: usize) -> Self {
290 Self { min_bars }
291 }
292}
293
294impl Condition for HeldForBars {
295 fn evaluate(&self, ctx: &StrategyContext) -> bool {
296 if let Some(pos) = ctx.position {
297 let entry_idx = ctx
299 .candles
300 .iter()
301 .position(|c| c.timestamp >= pos.entry_timestamp)
302 .unwrap_or(0);
303 let bars_held = ctx.index.saturating_sub(entry_idx);
304 bars_held >= self.min_bars
305 } else {
306 false
307 }
308 }
309
310 fn required_indicators(&self) -> Vec<(String, Indicator)> {
311 vec![]
312 }
313
314 fn description(&self) -> String {
315 format!("held for {} bars", self.min_bars)
316 }
317}
318
319#[inline]
321pub fn held_for_bars(min_bars: usize) -> HeldForBars {
322 HeldForBars::new(min_bars)
323}
324
325#[derive(Debug, Clone, Copy)]
342pub struct TrailingStop {
343 pub trail_pct: f64,
345}
346
347impl TrailingStop {
348 pub fn new(trail_pct: f64) -> Self {
354 Self { trail_pct }
355 }
356}
357
358impl Condition for TrailingStop {
359 fn evaluate(&self, ctx: &StrategyContext) -> bool {
360 if let Some(pos) = ctx.position {
361 let entry_idx = ctx
363 .candles
364 .iter()
365 .position(|c| c.timestamp >= pos.entry_timestamp)
366 .unwrap_or(0);
367
368 let current_close = ctx.close();
370
371 match pos.side {
372 crate::backtesting::position::PositionSide::Long => {
373 let peak = ctx.candles[entry_idx..=ctx.index]
375 .iter()
376 .map(|c| c.high)
377 .fold(f64::NEG_INFINITY, f64::max);
378
379 current_close <= peak * (1.0 - self.trail_pct)
381 }
382 crate::backtesting::position::PositionSide::Short => {
383 let trough = ctx.candles[entry_idx..=ctx.index]
385 .iter()
386 .map(|c| c.low)
387 .fold(f64::INFINITY, f64::min);
388
389 current_close >= trough * (1.0 + self.trail_pct)
391 }
392 }
393 } else {
394 false
395 }
396 }
397
398 fn required_indicators(&self) -> Vec<(String, Indicator)> {
399 vec![]
400 }
401
402 fn description(&self) -> String {
403 format!("trailing stop at {:.1}%", self.trail_pct * 100.0)
404 }
405}
406
407#[inline]
421pub fn trailing_stop(trail_pct: f64) -> TrailingStop {
422 TrailingStop::new(trail_pct)
423}
424
425#[derive(Debug, Clone, Copy)]
445pub struct TrailingTakeProfit {
446 pub trail_pct: f64,
448}
449
450impl TrailingTakeProfit {
451 pub fn new(trail_pct: f64) -> Self {
457 Self { trail_pct }
458 }
459}
460
461impl Condition for TrailingTakeProfit {
462 fn evaluate(&self, ctx: &StrategyContext) -> bool {
463 if let Some(pos) = ctx.position {
464 let entry_idx = ctx
466 .candles
467 .iter()
468 .position(|c| c.timestamp >= pos.entry_timestamp)
469 .unwrap_or(0);
470
471 let peak_profit_pct = ctx.candles[entry_idx..=ctx.index]
473 .iter()
474 .map(|c| pos.unrealized_return_pct(c.close))
475 .fold(f64::NEG_INFINITY, f64::max);
476
477 let current_profit_pct = pos.unrealized_return_pct(ctx.close());
479
480 let trail_threshold = self.trail_pct * 100.0;
482
483 peak_profit_pct > 0.0 && current_profit_pct <= peak_profit_pct - trail_threshold
484 } else {
485 false
486 }
487 }
488
489 fn required_indicators(&self) -> Vec<(String, Indicator)> {
490 vec![]
491 }
492
493 fn description(&self) -> String {
494 format!("trailing take profit at {:.1}%", self.trail_pct * 100.0)
495 }
496}
497
498#[inline]
513pub fn trailing_take_profit(trail_pct: f64) -> TrailingTakeProfit {
514 TrailingTakeProfit::new(trail_pct)
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 #[test]
522 fn test_stop_loss_description() {
523 let sl = stop_loss(0.05);
524 assert_eq!(sl.description(), "stop loss at 5.0%");
525 }
526
527 #[test]
528 fn test_take_profit_description() {
529 let tp = take_profit(0.10);
530 assert_eq!(tp.description(), "take profit at 10.0%");
531 }
532
533 #[test]
534 fn test_position_conditions_descriptions() {
535 assert_eq!(has_position().description(), "has position");
536 assert_eq!(no_position().description(), "no position");
537 assert_eq!(is_long().description(), "is long");
538 assert_eq!(is_short().description(), "is short");
539 assert_eq!(in_profit().description(), "in profit");
540 assert_eq!(in_loss().description(), "in loss");
541 }
542
543 #[test]
544 fn test_held_for_bars_description() {
545 let hfb = held_for_bars(5);
546 assert_eq!(hfb.description(), "held for 5 bars");
547 }
548
549 #[test]
550 fn test_trailing_stop_description() {
551 let ts = trailing_stop(0.03);
552 assert_eq!(ts.description(), "trailing stop at 3.0%");
553 }
554
555 #[test]
556 fn test_trailing_take_profit_description() {
557 let ttp = trailing_take_profit(0.02);
558 assert_eq!(ttp.description(), "trailing take profit at 2.0%");
559 }
560
561 #[test]
562 fn test_no_indicators_required() {
563 assert!(stop_loss(0.05).required_indicators().is_empty());
564 assert!(take_profit(0.10).required_indicators().is_empty());
565 assert!(has_position().required_indicators().is_empty());
566 assert!(no_position().required_indicators().is_empty());
567 assert!(trailing_stop(0.03).required_indicators().is_empty());
568 assert!(trailing_take_profit(0.02).required_indicators().is_empty());
569 }
570}