Buy and hold strategy
The simplest strategy: buy once and hold forever. Defined ingb-types/src/strategy.rs:263.
use gb_types::strategy::*;
use gb_types::market::MarketEvent;
use gb_types::orders::{Order, OrderEvent, Side};
use rust_decimal::Decimal;
#[derive(Debug, Clone)]
pub struct BuyAndHoldStrategy {
config: StrategyConfig,
initialized: bool,
position_opened: bool,
}
impl BuyAndHoldStrategy {
pub fn new() -> Self {
Self {
config: StrategyConfig::new(
"buy_and_hold".to_string(),
"Buy and Hold".to_string()
),
initialized: false,
position_opened: false,
}
}
}
impl Strategy for BuyAndHoldStrategy {
fn initialize(&mut self, config: &StrategyConfig) -> Result<(), String> {
self.config = config.clone();
self.initialized = true;
Ok(())
}
fn on_market_event(
&mut self,
event: &MarketEvent,
context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
// Only buy once on first market event
if !self.initialized || self.position_opened {
return Ok(vec![]);
}
if let Some(symbol) = self.config.symbols.first() {
if event.symbol() == symbol {
let available_cash = context.get_available_cash();
if let Some(price) = context.get_current_price(symbol) {
// Use 95% of available cash
let quantity = available_cash * Decimal::new(95, 2) / price;
let order = Order::market_order(
symbol.clone(),
Side::Buy,
quantity,
self.config.strategy_id.clone()
);
self.position_opened = true;
return Ok(vec![StrategyAction::PlaceOrder(order)]);
}
}
}
Ok(vec![])
}
fn on_order_event(
&mut self,
_event: &OrderEvent,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn on_day_end(
&mut self,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn on_stop(
&mut self,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn get_config(&self) -> &StrategyConfig {
&self.config
}
fn get_metrics(&self) -> StrategyMetrics {
StrategyMetrics::new(self.config.strategy_id.clone())
}
}
- Simple state tracking with boolean flags
- One-time order placement on first market event
- Position sizing based on available cash
Moving average crossover strategy
Buys when short MA crosses above long MA, sells when it crosses below. Defined ingb-types/src/strategy.rs:347.
#[derive(Debug, Clone)]
pub struct MovingAverageCrossoverStrategy {
config: StrategyConfig,
initialized: bool,
short_period: usize,
long_period: usize,
position_size: Decimal,
last_signal: Option<Signal>,
}
#[derive(Debug, Clone, PartialEq)]
enum Signal {
Buy,
Sell,
}
impl MovingAverageCrossoverStrategy {
pub fn new(short_period: usize, long_period: usize) -> Self {
let mut config = StrategyConfig::new(
"ma_crossover".to_string(),
"Moving Average Crossover".to_string()
);
config.set_parameter("short_period", short_period);
config.set_parameter("long_period", long_period);
config.set_parameter("position_size", 0.95f64);
Self {
config,
initialized: false,
short_period,
long_period,
position_size: Decimal::new(95, 2),
last_signal: None,
}
}
fn calculate_sma(&self, prices: &[Decimal], period: usize) -> Option<Decimal> {
if prices.len() < period {
return None;
}
let sum: Decimal = prices.iter().rev().take(period).sum();
Some(sum / Decimal::from(period))
}
fn get_recent_prices(
&self,
context: &StrategyContext,
symbol: &Symbol
) -> Vec<Decimal> {
if let Some(buffer) = context.get_market_data(symbol) {
buffer.get_bars(self.long_period + 1)
.iter()
.map(|bar| bar.close)
.collect()
} else {
Vec::new()
}
}
}
impl Strategy for MovingAverageCrossoverStrategy {
fn initialize(&mut self, config: &StrategyConfig) -> Result<(), String> {
self.config = config.clone();
self.short_period = config.get_parameter("short_period").unwrap_or(10);
self.long_period = config.get_parameter("long_period").unwrap_or(20);
self.position_size = config.get_parameter::<f64>("position_size")
.map(Decimal::from_f64_retain)
.flatten()
.unwrap_or(Decimal::new(95, 2));
self.initialized = true;
Ok(())
}
fn on_market_event(
&mut self,
event: &MarketEvent,
context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
if !self.initialized {
return Ok(vec![]);
}
let symbol = event.symbol();
let prices = self.get_recent_prices(context, symbol);
// Need enough data for long MA
if prices.len() < self.long_period {
return Ok(vec![]);
}
let short_ma = self.calculate_sma(&prices, self.short_period);
let long_ma = self.calculate_sma(&prices, self.long_period);
if let (Some(short), Some(long)) = (short_ma, long_ma) {
// Determine current signal
let current_signal = if short > long {
Some(Signal::Buy)
} else if short < long {
Some(Signal::Sell)
} else {
None
};
// Only act on signal changes
if current_signal != self.last_signal {
let mut actions = Vec::new();
match current_signal {
Some(Signal::Buy) => {
// Close short position if any
if let Some(position) = context.get_position(symbol) {
if position.quantity < Decimal::ZERO {
let close_order = Order::market_order(
symbol.clone(),
Side::Buy,
position.quantity.abs(),
self.config.strategy_id.clone()
);
actions.push(StrategyAction::PlaceOrder(close_order));
}
}
// Open long position
let cash = context.get_available_cash();
if let Some(price) = context.get_current_price(symbol) {
let quantity = (cash * self.position_size) / price;
if quantity > Decimal::ZERO {
let order = Order::market_order(
symbol.clone(),
Side::Buy,
quantity,
self.config.strategy_id.clone()
);
actions.push(StrategyAction::PlaceOrder(order));
}
}
},
Some(Signal::Sell) => {
// Close long position if any
if let Some(position) = context.get_position(symbol) {
if position.quantity > Decimal::ZERO {
let order = Order::market_order(
symbol.clone(),
Side::Sell,
position.quantity,
self.config.strategy_id.clone()
);
actions.push(StrategyAction::PlaceOrder(order));
}
}
},
None => {},
}
self.last_signal = current_signal;
return Ok(actions);
}
}
Ok(vec![])
}
fn on_order_event(
&mut self,
_event: &OrderEvent,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn on_day_end(
&mut self,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn on_stop(
&mut self,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn get_config(&self) -> &StrategyConfig {
&self.config
}
fn get_metrics(&self) -> StrategyMetrics {
StrategyMetrics::new(self.config.strategy_id.clone())
}
}
- Helper methods for calculations (
calculate_sma) - Signal state tracking to avoid redundant trades
- Closing opposite positions before opening new ones
- Configurable parameters with defaults
Momentum strategy
Buys when price momentum exceeds threshold, sells on negative momentum. Defined ingb-types/src/strategy.rs:541.
#[derive(Debug, Clone)]
pub struct MomentumStrategy {
config: StrategyConfig,
initialized: bool,
lookback_period: usize,
momentum_threshold: Decimal,
position_size: Decimal,
rebalance_frequency: usize,
days_since_rebalance: usize,
}
impl MomentumStrategy {
pub fn new(lookback_period: usize, momentum_threshold: f64) -> Self {
let mut config = StrategyConfig::new(
"momentum".to_string(),
"Momentum Strategy".to_string()
);
config.set_parameter("lookback_period", lookback_period);
config.set_parameter("momentum_threshold", momentum_threshold);
config.set_parameter("position_size", 0.95f64);
config.set_parameter("rebalance_frequency", 5);
Self {
config,
initialized: false,
lookback_period,
momentum_threshold: Decimal::from_f64_retain(momentum_threshold)
.unwrap_or(Decimal::new(5, 2)),
position_size: Decimal::new(95, 2),
rebalance_frequency: 5,
days_since_rebalance: 0,
}
}
fn calculate_momentum(&self, prices: &[Decimal]) -> Option<Decimal> {
if prices.len() < self.lookback_period {
return None;
}
let current_price = prices.last()?;
let past_price = prices.get(prices.len() - self.lookback_period)?;
// Calculate percentage change
if *past_price != Decimal::ZERO {
Some((*current_price - *past_price) / *past_price * Decimal::from(100))
} else {
None
}
}
}
impl Strategy for MomentumStrategy {
fn initialize(&mut self, config: &StrategyConfig) -> Result<(), String> {
self.config = config.clone();
self.lookback_period = config.get_parameter("lookback_period").unwrap_or(10);
self.momentum_threshold = config.get_parameter::<f64>("momentum_threshold")
.map(Decimal::from_f64_retain)
.flatten()
.unwrap_or(Decimal::new(5, 2));
self.rebalance_frequency = config.get_parameter("rebalance_frequency").unwrap_or(5);
self.initialized = true;
Ok(())
}
fn on_market_event(
&mut self,
event: &MarketEvent,
context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
if !self.initialized {
return Ok(vec![]);
}
// Only trade on rebalance days
if self.days_since_rebalance < self.rebalance_frequency {
return Ok(vec![]);
}
let symbol = event.symbol();
if let Some(buffer) = context.get_market_data(symbol) {
let bars = buffer.get_bars(self.lookback_period + 1);
let prices: Vec<Decimal> = bars.iter().map(|b| b.close).collect();
if let Some(momentum) = self.calculate_momentum(&prices) {
let mut actions = Vec::new();
let current_position = context.get_position(symbol);
if momentum > self.momentum_threshold {
// Strong positive momentum - go long
let target_quantity = if let Some(price) = context.get_current_price(symbol) {
(context.get_portfolio_value() * self.position_size) / price
} else {
Decimal::ZERO
};
let current_qty = current_position
.map(|p| p.quantity)
.unwrap_or(Decimal::ZERO);
let qty_diff = target_quantity - current_qty;
if qty_diff.abs() > Decimal::new(1, 4) {
let side = if qty_diff > Decimal::ZERO {
Side::Buy
} else {
Side::Sell
};
let order = Order::market_order(
symbol.clone(),
side,
qty_diff.abs(),
self.config.strategy_id.clone()
);
actions.push(StrategyAction::PlaceOrder(order));
}
} else if momentum < -self.momentum_threshold {
// Strong negative momentum - close positions
if let Some(position) = current_position {
if position.quantity > Decimal::ZERO {
let order = Order::market_order(
symbol.clone(),
Side::Sell,
position.quantity,
self.config.strategy_id.clone()
);
actions.push(StrategyAction::PlaceOrder(order));
}
}
}
self.days_since_rebalance = 0;
return Ok(actions);
}
}
Ok(vec![])
}
fn on_order_event(
&mut self,
_event: &OrderEvent,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn on_day_end(
&mut self,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
// Increment day counter for rebalancing
self.days_since_rebalance += 1;
Ok(vec![])
}
fn on_stop(
&mut self,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn get_config(&self) -> &StrategyConfig {
&self.config
}
fn get_metrics(&self) -> StrategyMetrics {
StrategyMetrics::new(self.config.strategy_id.clone())
}
}
- Periodic rebalancing using
on_day_end - Momentum calculation over configurable periods
- Target position sizing vs. current position
- Minimum trade size threshold to avoid tiny orders
Mean reversion strategy
Buys when price falls below mean, sells when above mean. Uses z-scores. Defined ingb-types/src/strategy.rs:701.
#[derive(Debug, Clone)]
pub struct MeanReversionStrategy {
config: StrategyConfig,
initialized: bool,
lookback_period: usize,
entry_threshold: Decimal, // Z-score threshold for entry
exit_threshold: Decimal, // Z-score threshold for exit
position_size: Decimal,
max_position_size: Decimal,
}
impl MeanReversionStrategy {
pub fn new(
lookback_period: usize,
entry_threshold: f64,
exit_threshold: f64
) -> Self {
let mut config = StrategyConfig::new(
"mean_reversion".to_string(),
"Mean Reversion Strategy".to_string()
);
config.set_parameter("lookback_period", lookback_period);
config.set_parameter("entry_threshold", entry_threshold);
config.set_parameter("exit_threshold", exit_threshold);
config.set_parameter("position_size", 0.25f64);
config.set_parameter("max_position_size", 0.95f64);
Self {
config,
initialized: false,
lookback_period,
entry_threshold: Decimal::from_f64_retain(entry_threshold)
.unwrap_or(Decimal::from(2)),
exit_threshold: Decimal::from_f64_retain(exit_threshold)
.unwrap_or(Decimal::from(1)),
position_size: Decimal::new(25, 2),
max_position_size: Decimal::new(95, 2),
}
}
fn calculate_z_score(&self, prices: &[Decimal]) -> Option<Decimal> {
if prices.len() < self.lookback_period {
return None;
}
let recent: Vec<Decimal> = prices.iter()
.rev()
.take(self.lookback_period)
.cloned()
.collect();
let current_price = *prices.last()?;
// Calculate mean
let mean = recent.iter().sum::<Decimal>() / Decimal::from(recent.len());
// Calculate standard deviation
let variance = recent.iter()
.map(|p| (*p - mean) * (*p - mean))
.sum::<Decimal>() / Decimal::from(recent.len());
let std_dev = variance.to_f64()
.map(|v| v.sqrt())
.and_then(Decimal::from_f64)
.unwrap_or(Decimal::ZERO);
if std_dev != Decimal::ZERO {
Some((current_price - mean) / std_dev)
} else {
None
}
}
}
impl Strategy for MeanReversionStrategy {
fn initialize(&mut self, config: &StrategyConfig) -> Result<(), String> {
self.config = config.clone();
self.lookback_period = config.get_parameter("lookback_period").unwrap_or(20);
self.entry_threshold = config.get_parameter::<f64>("entry_threshold")
.map(Decimal::from_f64_retain)
.flatten()
.unwrap_or(Decimal::from(2));
self.exit_threshold = config.get_parameter::<f64>("exit_threshold")
.map(Decimal::from_f64_retain)
.flatten()
.unwrap_or(Decimal::from(1));
self.initialized = true;
Ok(())
}
fn on_market_event(
&mut self,
event: &MarketEvent,
context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
if !self.initialized {
return Ok(vec![]);
}
let symbol = event.symbol();
if let Some(buffer) = context.get_market_data(symbol) {
let bars = buffer.get_bars(self.lookback_period + 5);
let prices: Vec<Decimal> = bars.iter().map(|b| b.close).collect();
if let Some(z_score) = self.calculate_z_score(&prices) {
let mut actions = Vec::new();
let current_qty = context.get_position(symbol)
.map(|p| p.quantity)
.unwrap_or(Decimal::ZERO);
// Entry: price significantly below mean
if z_score < -self.entry_threshold {
let cash = context.get_available_cash();
if let Some(price) = context.get_current_price(symbol) {
let max_qty = (context.get_portfolio_value() * self.max_position_size) / price;
let increment = (context.get_portfolio_value() * self.position_size) / price;
if current_qty < max_qty {
let qty = (max_qty - current_qty).min(increment);
if qty > Decimal::new(1, 4) && cash > qty * price {
let order = Order::market_order(
symbol.clone(),
Side::Buy,
qty,
self.config.strategy_id.clone()
);
actions.push(StrategyAction::PlaceOrder(order));
}
}
}
}
// Exit: price moving back toward mean
if current_qty > Decimal::ZERO && z_score > -self.exit_threshold {
let exit_qty = current_qty.min(
(context.get_portfolio_value() * self.position_size) /
context.get_current_price(symbol).unwrap_or(Decimal::ONE)
);
if exit_qty > Decimal::new(1, 4) {
let order = Order::market_order(
symbol.clone(),
Side::Sell,
exit_qty,
self.config.strategy_id.clone()
);
actions.push(StrategyAction::PlaceOrder(order));
}
}
return Ok(actions);
}
}
Ok(vec![])
}
fn on_order_event(
&mut self,
_event: &OrderEvent,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn on_day_end(
&mut self,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn on_stop(
&mut self,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn get_config(&self) -> &StrategyConfig {
&self.config
}
fn get_metrics(&self) -> StrategyMetrics {
StrategyMetrics::new(self.config.strategy_id.clone())
}
}
- Statistical calculations (z-score, standard deviation)
- Incremental position building up to a maximum
- Separate entry and exit thresholds
- Smaller position sizes for mean-reverting strategies
RSI strategy
Buys when RSI indicates oversold, sells when overbought. Defined ingb-types/src/strategy.rs:712.
#[derive(Debug, Clone)]
pub struct RsiStrategy {
config: StrategyConfig,
initialized: bool,
lookback_period: usize,
oversold_threshold: Decimal,
overbought_threshold: Decimal,
position_size: Decimal,
}
impl RsiStrategy {
pub fn new(
lookback_period: usize,
oversold_threshold: f64,
overbought_threshold: f64
) -> Self {
let mut config = StrategyConfig::new(
"rsi".to_string(),
"RSI Strategy".to_string()
);
config.set_parameter("lookback_period", lookback_period);
config.set_parameter("oversold_threshold", oversold_threshold);
config.set_parameter("overbought_threshold", overbought_threshold);
config.set_parameter("position_size", 0.95f64);
Self {
config,
initialized: false,
lookback_period,
oversold_threshold: Decimal::from_f64_retain(oversold_threshold)
.unwrap_or(Decimal::from(30)),
overbought_threshold: Decimal::from_f64_retain(overbought_threshold)
.unwrap_or(Decimal::from(70)),
position_size: Decimal::new(95, 2),
}
}
fn calculate_rsi(&self, prices: &[Decimal]) -> Option<Decimal> {
if prices.len() < self.lookback_period + 1 {
return None;
}
let mut gains = Decimal::ZERO;
let mut losses = Decimal::ZERO;
let recent: Vec<Decimal> = prices.iter()
.rev()
.take(self.lookback_period + 1)
.cloned()
.collect();
let mut recent_iter = recent.into_iter().rev();
let mut previous = recent_iter.next()?;
for price in recent_iter {
let change = price - previous;
if change > Decimal::ZERO {
gains += change;
} else if change < Decimal::ZERO {
losses += change.abs();
}
previous = price;
}
let period = Decimal::from(self.lookback_period);
let avg_gain = gains / period;
let avg_loss = losses / period;
if avg_loss == Decimal::ZERO {
return Some(Decimal::from(100));
}
if avg_gain == Decimal::ZERO {
return Some(Decimal::ZERO);
}
let rs = avg_gain / avg_loss;
let rsi = Decimal::from(100) - (Decimal::from(100) / (Decimal::ONE + rs));
Some(rsi)
}
}
impl Strategy for RsiStrategy {
fn initialize(&mut self, config: &StrategyConfig) -> Result<(), String> {
self.config = config.clone();
self.lookback_period = config.get_parameter("lookback_period").unwrap_or(14);
self.oversold_threshold = config.get_parameter::<f64>("oversold_threshold")
.map(Decimal::from_f64_retain)
.flatten()
.unwrap_or(Decimal::from(30));
self.overbought_threshold = config.get_parameter::<f64>("overbought_threshold")
.map(Decimal::from_f64_retain)
.flatten()
.unwrap_or(Decimal::from(70));
self.initialized = true;
Ok(())
}
fn on_market_event(
&mut self,
event: &MarketEvent,
context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
if !self.initialized {
return Ok(vec![]);
}
let symbol = event.symbol();
if let Some(buffer) = context.get_market_data(symbol) {
let bars = buffer.get_bars(self.lookback_period + 1);
let prices: Vec<Decimal> = bars.iter().map(|b| b.close).collect();
if let Some(rsi) = self.calculate_rsi(&prices) {
let mut actions = Vec::new();
let current_qty = context.get_position(symbol)
.map(|p| p.quantity)
.unwrap_or(Decimal::ZERO);
if rsi < self.oversold_threshold {
// Oversold - buy signal
let target_qty = if let Some(price) = context.get_current_price(symbol) {
(context.get_portfolio_value() * self.position_size) / price
} else {
Decimal::ZERO
};
let qty_diff = target_qty - current_qty;
if qty_diff > Decimal::new(1, 4) {
let order = Order::market_order(
symbol.clone(),
Side::Buy,
qty_diff,
self.config.strategy_id.clone()
);
actions.push(StrategyAction::PlaceOrder(order));
}
} else if rsi > self.overbought_threshold {
// Overbought - sell signal
if current_qty > Decimal::ZERO {
let order = Order::market_order(
symbol.clone(),
Side::Sell,
current_qty,
self.config.strategy_id.clone()
);
actions.push(StrategyAction::PlaceOrder(order));
}
}
return Ok(actions);
}
}
Ok(vec![])
}
fn on_order_event(
&mut self,
_event: &OrderEvent,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn on_day_end(
&mut self,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn on_stop(
&mut self,
_context: &StrategyContext,
) -> Result<Vec<StrategyAction>, String> {
Ok(vec![])
}
fn get_config(&self) -> &StrategyConfig {
&self.config
}
fn get_metrics(&self) -> StrategyMetrics {
StrategyMetrics::new(self.config.strategy_id.clone())
}
}
- Complex indicator calculation (RSI)
- Handling edge cases (zero division)
- Directional signals (oversold vs. overbought)
Common patterns
Checking for sufficient data
let bars = buffer.get_bars(self.lookback_period);
if bars.len() < self.lookback_period {
return Ok(vec![]); // Not enough data yet
}
Position sizing
// Fixed percentage of portfolio
let target_value = context.get_portfolio_value() * Decimal::new(20, 2); // 20%
let quantity = target_value / price;
// Fixed percentage of cash
let cash_to_use = context.get_available_cash() * Decimal::new(95, 2); // 95%
let quantity = cash_to_use / price;
Closing positions
if let Some(position) = context.get_position(symbol) {
if position.quantity > Decimal::ZERO {
// Close long position
let order = Order::market_order(
symbol.clone(),
Side::Sell,
position.quantity,
strategy_id
);
} else if position.quantity < Decimal::ZERO {
// Close short position
let order = Order::market_order(
symbol.clone(),
Side::Buy,
position.quantity.abs(),
strategy_id
);
}
}
Minimum trade size
let min_trade_size = Decimal::new(1, 4); // 0.0001
if quantity.abs() > min_trade_size {
// Place order
}
See also
Creating strategies
Step-by-step implementation guide
Strategy interface
Complete trait documentation