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(¶ms);
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
}
}