Skip to content

Examples

Bollinger Band Bounce

Buy when price touches the lower band, sell when it touches the upper band.

rust
use tradectl_sdk::{Strategy, Action, Side, TickerEvent, StrategyContext, Params, ParamDef};
use tradectl_sdk::indicators::BollingerBands;

tradectl_sdk::declare_strategy!("bb_bounce", BollingerBounce::new);

pub struct BollingerBounce {
    bb: BollingerBands,
    order_size: f64,
}

impl BollingerBounce {
    pub fn new(params: &Params) -> Self {
        Self {
            bb: BollingerBands::new(
                params.get("period", 20.0) as usize,
                params.get("std_dev", 2.0),
            ),
            order_size: params.get("order_size", 0.1),
        }
    }
}

impl Strategy for BollingerBounce {
    fn on_ticker(&mut self, ticker: &TickerEvent, ctx: &StrategyContext) -> Action {
        let mid = (ticker.bid_price + ticker.ask_price) * 0.5;
        self.bb.update(mid);

        if !self.bb.ready() {
            return Action::Hold;
        }

        if ctx.positions.is_empty() {
            if mid <= self.bb.lower() {
                return Action::MarketOpen {
                    side: Side::Long,
                    size: Some(self.order_size),
                };
            }
            if mid >= self.bb.upper() {
                return Action::MarketOpen {
                    side: Side::Short,
                    size: Some(self.order_size),
                };
            }
        }

        Action::Hold
    }

    fn name(&self) -> &str { "bb_bounce" }
    fn describe(&self) -> &str { "Bollinger Band bounce strategy" }

    fn params_schema(&self) -> Vec<ParamDef> {
        vec![
            ParamDef { key: "period", description: "BB period", default: 20.0, min: Some(5.0), max: Some(50.0), step: Some(1.0) },
            ParamDef { key: "std_dev", description: "Standard deviation multiplier", default: 2.0, min: Some(1.0), max: Some(4.0), step: Some(0.1) },
            ParamDef { key: "order_size", description: "Order size", default: 0.1, min: None, max: None, step: None },
        ]
    }
}

RSI Mean Reversion

Buy oversold, close when neutral.

rust
use tradectl_sdk::{Strategy, Action, Side, TickerEvent, StrategyContext, Params, ParamDef};
use tradectl_sdk::indicators::Rsi;

tradectl_sdk::declare_strategy!("rsi_reversion", RsiReversion::new);

pub struct RsiReversion {
    rsi: Rsi,
    oversold: f64,
    overbought: f64,
    order_size: f64,
}

impl RsiReversion {
    pub fn new(params: &Params) -> Self {
        Self {
            rsi: Rsi::new(params.get("period", 14.0) as usize),
            oversold: params.get("oversold", 30.0),
            overbought: params.get("overbought", 70.0),
            order_size: params.get("order_size", 0.1),
        }
    }
}

impl Strategy for RsiReversion {
    fn on_ticker(&mut self, ticker: &TickerEvent, ctx: &StrategyContext) -> Action {
        let mid = (ticker.bid_price + ticker.ask_price) * 0.5;
        self.rsi.update(mid);

        if !self.rsi.ready() {
            return Action::Hold;
        }

        let rsi = self.rsi.value();

        if ctx.positions.is_empty() {
            if rsi < self.oversold {
                return Action::MarketOpen {
                    side: Side::Long,
                    size: Some(self.order_size),
                };
            }
            if rsi > self.overbought {
                return Action::MarketOpen {
                    side: Side::Short,
                    size: Some(self.order_size),
                };
            }
        } else {
            // Close when RSI returns to neutral
            if rsi > 50.0 && ctx.positions.iter().any(|p| p.side == Side::Long) {
                return Action::CloseAll;
            }
            if rsi < 50.0 && ctx.positions.iter().any(|p| p.side == Side::Short) {
                return Action::CloseAll;
            }
        }

        Action::Hold
    }

    fn name(&self) -> &str { "rsi_reversion" }
    fn describe(&self) -> &str { "RSI mean reversion strategy" }

    fn params_schema(&self) -> Vec<ParamDef> {
        vec![
            ParamDef { key: "period", description: "RSI period", default: 14.0, min: Some(5.0), max: Some(50.0), step: Some(1.0) },
            ParamDef { key: "oversold", description: "Oversold threshold", default: 30.0, min: Some(10.0), max: Some(40.0), step: Some(5.0) },
            ParamDef { key: "overbought", description: "Overbought threshold", default: 70.0, min: Some(60.0), max: Some(90.0), step: Some(5.0) },
            ParamDef { key: "order_size", description: "Order size", default: 0.1, min: None, max: None, step: None },
        ]
    }
}

Testing with TestExchange

rust
#[cfg(test)]
mod tests {
    use tradectl_sdk::{Params, TickerEvent, StrategyContext, Strategy, Action};
    use super::BollingerBounce;

    #[test]
    fn returns_hold_when_not_ready() {
        let params = Params::new()
            .set("period", 20.0)
            .set("std_dev", 2.0)
            .set("order_size", 0.1);

        let mut strategy = BollingerBounce::new(&params);

        let ticker = TickerEvent {
            bid_price: 50000.0,
            bid_qty: 1.0,
            ask_price: 50001.0,
            ask_qty: 1.0,
            timestamp_ms: 1000,
        };

        let ctx = StrategyContext {
            timestamp_ms: 1000,
            book: Some(&ticker),
            positions: &[],
            balance: 10000.0,
            unrealized_pnl: 0.0,
            realized_pnl: 0.0,
            trade_count: 0,
        };

        let action = strategy.on_ticker(&ticker, &ctx);
        assert!(matches!(action, Action::Hold)); // Not enough data for BB
    }
}

tradectl — Automate Crypto Trading