use crate::backtesting::strategy::StrategyContext;
use crate::indicators::Indicator;
use super::Condition;
#[derive(Debug, Clone, Copy)]
pub struct StopLoss {
pub pct: f64,
}
impl StopLoss {
pub fn new(pct: f64) -> Self {
Self { pct }
}
}
impl Condition for StopLoss {
fn evaluate(&self, ctx: &StrategyContext) -> bool {
if let Some(pos) = ctx.position {
let pnl_pct = pos.unrealized_return_pct(ctx.close()) / 100.0;
pnl_pct <= -self.pct
} else {
false
}
}
fn required_indicators(&self) -> Vec<(String, Indicator)> {
vec![]
}
fn description(&self) -> String {
format!("stop loss at {:.1}%", self.pct * 100.0)
}
}
#[inline]
pub fn stop_loss(pct: f64) -> StopLoss {
StopLoss::new(pct)
}
#[derive(Debug, Clone, Copy)]
pub struct TakeProfit {
pub pct: f64,
}
impl TakeProfit {
pub fn new(pct: f64) -> Self {
Self { pct }
}
}
impl Condition for TakeProfit {
fn evaluate(&self, ctx: &StrategyContext) -> bool {
if let Some(pos) = ctx.position {
let pnl_pct = pos.unrealized_return_pct(ctx.close()) / 100.0;
pnl_pct >= self.pct
} else {
false
}
}
fn required_indicators(&self) -> Vec<(String, Indicator)> {
vec![]
}
fn description(&self) -> String {
format!("take profit at {:.1}%", self.pct * 100.0)
}
}
#[inline]
pub fn take_profit(pct: f64) -> TakeProfit {
TakeProfit::new(pct)
}
#[derive(Debug, Clone, Copy)]
pub struct HasPosition;
impl Condition for HasPosition {
fn evaluate(&self, ctx: &StrategyContext) -> bool {
ctx.has_position()
}
fn required_indicators(&self) -> Vec<(String, Indicator)> {
vec![]
}
fn description(&self) -> String {
"has position".to_string()
}
}
#[inline]
pub fn has_position() -> HasPosition {
HasPosition
}
#[derive(Debug, Clone, Copy)]
pub struct NoPosition;
impl Condition for NoPosition {
fn evaluate(&self, ctx: &StrategyContext) -> bool {
!ctx.has_position()
}
fn required_indicators(&self) -> Vec<(String, Indicator)> {
vec![]
}
fn description(&self) -> String {
"no position".to_string()
}
}
#[inline]
pub fn no_position() -> NoPosition {
NoPosition
}
#[derive(Debug, Clone, Copy)]
pub struct IsLong;
impl Condition for IsLong {
fn evaluate(&self, ctx: &StrategyContext) -> bool {
ctx.is_long()
}
fn required_indicators(&self) -> Vec<(String, Indicator)> {
vec![]
}
fn description(&self) -> String {
"is long".to_string()
}
}
#[inline]
pub fn is_long() -> IsLong {
IsLong
}
#[derive(Debug, Clone, Copy)]
pub struct IsShort;
impl Condition for IsShort {
fn evaluate(&self, ctx: &StrategyContext) -> bool {
ctx.is_short()
}
fn required_indicators(&self) -> Vec<(String, Indicator)> {
vec![]
}
fn description(&self) -> String {
"is short".to_string()
}
}
#[inline]
pub fn is_short() -> IsShort {
IsShort
}
#[derive(Debug, Clone, Copy)]
pub struct InProfit;
impl Condition for InProfit {
fn evaluate(&self, ctx: &StrategyContext) -> bool {
if let Some(pos) = ctx.position {
pos.unrealized_return_pct(ctx.close()) > 0.0
} else {
false
}
}
fn required_indicators(&self) -> Vec<(String, Indicator)> {
vec![]
}
fn description(&self) -> String {
"in profit".to_string()
}
}
#[inline]
pub fn in_profit() -> InProfit {
InProfit
}
#[derive(Debug, Clone, Copy)]
pub struct InLoss;
impl Condition for InLoss {
fn evaluate(&self, ctx: &StrategyContext) -> bool {
if let Some(pos) = ctx.position {
pos.unrealized_return_pct(ctx.close()) < 0.0
} else {
false
}
}
fn required_indicators(&self) -> Vec<(String, Indicator)> {
vec![]
}
fn description(&self) -> String {
"in loss".to_string()
}
}
#[inline]
pub fn in_loss() -> InLoss {
InLoss
}
#[derive(Debug, Clone, Copy)]
pub struct HeldForBars {
pub min_bars: usize,
}
impl HeldForBars {
pub fn new(min_bars: usize) -> Self {
Self { min_bars }
}
}
impl Condition for HeldForBars {
fn evaluate(&self, ctx: &StrategyContext) -> bool {
if let Some(pos) = ctx.position {
let entry_idx = ctx
.candles
.iter()
.position(|c| c.timestamp >= pos.entry_timestamp)
.unwrap_or(0);
let bars_held = ctx.index.saturating_sub(entry_idx);
bars_held >= self.min_bars
} else {
false
}
}
fn required_indicators(&self) -> Vec<(String, Indicator)> {
vec![]
}
fn description(&self) -> String {
format!("held for {} bars", self.min_bars)
}
}
#[inline]
pub fn held_for_bars(min_bars: usize) -> HeldForBars {
HeldForBars::new(min_bars)
}
#[derive(Debug, Clone, Copy)]
pub struct TrailingStop {
pub trail_pct: f64,
}
impl TrailingStop {
pub fn new(trail_pct: f64) -> Self {
Self { trail_pct }
}
}
impl Condition for TrailingStop {
fn evaluate(&self, ctx: &StrategyContext) -> bool {
if let Some(pos) = ctx.position {
let entry_idx = ctx
.candles
.iter()
.position(|c| c.timestamp >= pos.entry_timestamp)
.unwrap_or(0);
let current_close = ctx.close();
match pos.side {
crate::backtesting::position::PositionSide::Long => {
let peak = ctx.candles[entry_idx..=ctx.index]
.iter()
.map(|c| c.high)
.fold(f64::NEG_INFINITY, f64::max);
current_close <= peak * (1.0 - self.trail_pct)
}
crate::backtesting::position::PositionSide::Short => {
let trough = ctx.candles[entry_idx..=ctx.index]
.iter()
.map(|c| c.low)
.fold(f64::INFINITY, f64::min);
current_close >= trough * (1.0 + self.trail_pct)
}
}
} else {
false
}
}
fn required_indicators(&self) -> Vec<(String, Indicator)> {
vec![]
}
fn description(&self) -> String {
format!("trailing stop at {:.1}%", self.trail_pct * 100.0)
}
}
#[inline]
pub fn trailing_stop(trail_pct: f64) -> TrailingStop {
TrailingStop::new(trail_pct)
}
#[derive(Debug, Clone, Copy)]
pub struct TrailingTakeProfit {
pub trail_pct: f64,
}
impl TrailingTakeProfit {
pub fn new(trail_pct: f64) -> Self {
Self { trail_pct }
}
}
impl Condition for TrailingTakeProfit {
fn evaluate(&self, ctx: &StrategyContext) -> bool {
if let Some(pos) = ctx.position {
let entry_idx = ctx
.candles
.iter()
.position(|c| c.timestamp >= pos.entry_timestamp)
.unwrap_or(0);
let peak_profit_pct = ctx.candles[entry_idx..=ctx.index]
.iter()
.map(|c| pos.unrealized_return_pct(c.close))
.fold(f64::NEG_INFINITY, f64::max);
let current_profit_pct = pos.unrealized_return_pct(ctx.close());
let trail_threshold = self.trail_pct * 100.0;
peak_profit_pct > 0.0 && current_profit_pct <= peak_profit_pct - trail_threshold
} else {
false
}
}
fn required_indicators(&self) -> Vec<(String, Indicator)> {
vec![]
}
fn description(&self) -> String {
format!("trailing take profit at {:.1}%", self.trail_pct * 100.0)
}
}
#[inline]
pub fn trailing_take_profit(trail_pct: f64) -> TrailingTakeProfit {
TrailingTakeProfit::new(trail_pct)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stop_loss_description() {
let sl = stop_loss(0.05);
assert_eq!(sl.description(), "stop loss at 5.0%");
}
#[test]
fn test_take_profit_description() {
let tp = take_profit(0.10);
assert_eq!(tp.description(), "take profit at 10.0%");
}
#[test]
fn test_position_conditions_descriptions() {
assert_eq!(has_position().description(), "has position");
assert_eq!(no_position().description(), "no position");
assert_eq!(is_long().description(), "is long");
assert_eq!(is_short().description(), "is short");
assert_eq!(in_profit().description(), "in profit");
assert_eq!(in_loss().description(), "in loss");
}
#[test]
fn test_held_for_bars_description() {
let hfb = held_for_bars(5);
assert_eq!(hfb.description(), "held for 5 bars");
}
#[test]
fn test_trailing_stop_description() {
let ts = trailing_stop(0.03);
assert_eq!(ts.description(), "trailing stop at 3.0%");
}
#[test]
fn test_trailing_take_profit_description() {
let ttp = trailing_take_profit(0.02);
assert_eq!(ttp.description(), "trailing take profit at 2.0%");
}
#[test]
fn test_no_indicators_required() {
assert!(stop_loss(0.05).required_indicators().is_empty());
assert!(take_profit(0.10).required_indicators().is_empty());
assert!(has_position().required_indicators().is_empty());
assert!(no_position().required_indicators().is_empty());
assert!(trailing_stop(0.03).required_indicators().is_empty());
assert!(trailing_take_profit(0.02).required_indicators().is_empty());
}
}