Skip to content

Reference Guide: Trading Strategies

Overview

Strategies in TzuTrader analyze market data and generate trading signals. The framework provides a base Strategy class for building custom strategies and includes 16 pre-built strategies for common trading approaches.

A strategy's core responsibility is simple: given market data, decide whether to buy, sell, or stay out. TzuTrader handles everything else—executing trades, managing positions, tracking performance.

Module: tzutrader/strategy.nim

Strategy Categories

TzuTrader's 16 pre-built strategies are organized into categories. Each category has its own detailed reference guide:

Mean Reversion Strategies (6 strategies)

Strategies that trade price extremes, assuming reversion to average: - RSI Strategy - Classic overbought/oversold (documented below) - Bollinger Bands Strategy - Volatility-adjusted extremes (documented below) - Stochastic Strategy - Position within recent range - MFI Strategy - Volume-weighted momentum - CCI Strategy - Statistical deviation from mean - Filtered Mean Reversion - RSI with trend filter

Trend Following Strategies (7 strategies)

Strategies that ride sustained directional moves: - Moving Average Crossover - Golden/death crosses (documented below) - MACD Strategy - Momentum-based trend detection (documented below) - KAMA Strategy - Adaptive moving average - Aroon Strategy - Time-based trend identification - Parabolic SAR - Trailing stop trend follower - Triple MA Strategy - Multi-timeframe confirmation - ADX Trend Strategy - Strength-filtered trends

Hybrid & Volatility Strategies (3 strategies)

Strategies combining multiple confirmations or trading volatility: - Keltner Channel Strategy - ATR-based (dual mode: breakout/reversion) - Volume Breakout Strategy - Price + volume confirmation - Dual Momentum Strategy - ROC + trend filter

This document covers the strategy interface and the original 4 strategies. See the linked category guides for documentation on the additional 12 strategies.

Strategy Fundamentals

The Strategy Interface

All strategies inherit from the base Strategy class and implement the streaming interface:

method onBar*(s: Strategy, bar: OHLCV): Signal

Streaming-only design: - Strategies process bars one at a time as they arrive - Same code works for backtesting and live trading - O(1) memory usage - never grows with data size - Indicators maintain state internally

Base Strategy Type

type
  Strategy* = ref object of RootObj
    name*: string
    symbol*: string

Fields: - name: Human-readable strategy identifier - symbol: Target symbol (optional, can trade multiple symbols)

Method Specification

onBar (Streaming)

method onBar*(s: Strategy, bar: OHLCV): Signal

Purpose: Process a single bar and generate a trading signal.

Parameters: - bar: Single OHLCV bar to process

Returns: Signal with position recommendation (Buy, Sell, or Stay)

Use cases: - Backtesting (process historical data sequentially) - Live trading (process real-time data) - Strategy development and testing

State management: Strategies maintain internal state (indicator values, last signals, etc.) across onBar() calls.

Example:

let strategy = newRSIStrategy()

for bar in data:
  let signal = strategy.onBar(bar)
  if signal.position != Stay:
    echo signal.timestamp.fromUnix.format("yyyy-MM-dd"), ": ", signal.reason
echo "BUY signal at ", bar.close
#### reset

```nim
method reset*(s: Strategy)

Purpose: Clear strategy state for starting fresh.

When to call: - Before beginning a new streaming session - Between backtests using the same strategy instance - When switching symbols

What gets reset: - Indicator states - Bar history - Last signal tracking - Any accumulated state

Pre-Built Strategies (Original Four)

This section documents the original four strategies in detail. For the additional 12 strategies added in recent versions, see the category-specific guides linked above.

The original four strategies cover the fundamental trading approaches:

RSI Strategy

Mean reversion strategy using the Relative Strength Index.

Trading Logic: - Buy: RSI falls below oversold threshold - Sell: RSI rises above overbought threshold - Stay: RSI between thresholds

Constructor:

proc newRSIStrategy*(period: int = 14, oversold: float64 = 30.0, 
                     overbought: float64 = 70.0, symbol: string = ""): RSIStrategy

Parameters:

Parameter Type Default Description Typical Range
period int 14 RSI calculation period 9-21
oversold float64 30.0 Buy threshold 20-35
overbought float64 70.0 Sell threshold 65-80
symbol string "" Target symbol

Type:

type
  RSIStrategy* = ref object of Strategy
    period*: int
    oversold*: float64
    overbought*: float64
    rsiIndicator*: RSI
    lastSignal*: Position

Strategy Behavior:

The RSI strategy assumes markets oscillate around a mean. When RSI drops below 30 (default), the market is "oversold" and likely to bounce back—time to buy. When RSI exceeds 70, the market is "overbought" and likely to retreat—time to sell.

Signal Generation:

Signals are generated only when RSI crosses thresholds. The lastSignal field prevents repeated signals at the same threshold. You get one buy signal when entering oversold territory, not continuous buy signals while remaining oversold.

Parameter Selection:

  • Shorter periods (9-12): More responsive, more trades, more false signals
  • Longer periods (16-21): Smoother, fewer trades, fewer false signals
  • Tighter thresholds (25/75): Trade more frequently with smaller moves
  • Wider thresholds (20/80): Wait for extreme conditions, trade less

Example:

import tzutrader

# Conservative RSI strategy (wait for extremes)
let conservative = newRSIStrategy(
  period = 14,
  oversold = 20.0,
  overbought = 80.0
)

# Aggressive RSI strategy (trade more often)
let aggressive = newRSIStrategy(
  period = 10,
  oversold = 35.0,
  overbought = 65.0
)

let data = readCSV("data/AAPL.csv")
let reportConservative = quickBacktest("AAPL", conservative, data)
let reportAggressive = quickBacktest("AAPL", aggressive, data)

echo "Conservative: ", reportConservative.totalTrades, " trades, ", 
     reportConservative.totalReturn, "% return"
echo "Aggressive: ", reportAggressive.totalTrades, " trades, ", 
     reportAggressive.totalReturn, "% return"

Moving Average Crossover Strategy

Trend-following strategy based on two moving averages.

Trading Logic: - Buy: Fast MA crosses above slow MA (golden cross) - Sell: Fast MA crosses below slow MA (death cross) - Stay: No crossover

Constructor:

proc newCrossoverStrategy*(fastPeriod: int = 50, slowPeriod: int = 200,
                           symbol: string = ""): CrossoverStrategy

Parameters:

Parameter Type Default Description Typical Range
fastPeriod int 50 Fast MA period 10-100
slowPeriod int 200 Slow MA period 50-300
symbol string "" Target symbol

Type:

type
  CrossoverStrategy* = ref object of Strategy
    fastPeriod*: int
    slowPeriod*: int
    fastMA*: SMA
    slowMA*: SMA
    lastFastAbove*: bool

Strategy Behavior:

The crossover strategy identifies trend changes. When the fast MA (which responds quickly to price) crosses above the slow MA (which changes gradually), it suggests an uptrend is beginning. The opposite crossing suggests a downtrend.

Classic Combinations:

  • 50/200 (Golden Cross): The most famous combination, used by institutions
  • 10/30: Shorter-term trading, more signals
  • 20/50: Medium-term trading, balanced
  • Fast/Slow ratio ~4x: Provides clear separation

Signal Generation:

Crossovers are discrete events. You get one buy signal when the cross occurs, not continuous signals while fast remains above slow. The lastFastAbove field tracks the relationship state.

Lag Consideration:

Moving averages lag price by design. By the time a crossover occurs, the trend may already be well underway. The strategy sacrifices early entry for confirmation that a trend exists.

Example:

import tzutrader

# Short-term trading (faster signals)
let shortTerm = newCrossoverStrategy(fastPeriod = 20, slowPeriod = 50)

# Long-term position trading (fewer, stronger signals)
let longTerm = newCrossoverStrategy(fastPeriod = 50, slowPeriod = 200)

let data = readCSV("data/AAPL.csv")
let report = quickBacktest("AAPL", longTerm, data)

echo "Trades: ", report.totalTrades
echo "Win rate: ", report.winRate, "%"

MACD Strategy

Momentum strategy using Moving Average Convergence Divergence.

Trading Logic: - Buy: MACD line crosses above signal line (bullish crossover) - Sell: MACD line crosses below signal line (bearish crossover) - Stay: No crossover

Constructor:

proc newMACDStrategy*(fastPeriod: int = 12, slowPeriod: int = 26,
                      signalPeriod: int = 9, symbol: string = ""): MACDStrategy

Parameters:

Parameter Type Default Description Typical Range
fastPeriod int 12 Fast EMA period 8-15
slowPeriod int 26 Slow EMA period 20-30
signalPeriod int 9 Signal line period 7-12
symbol string "" Target symbol

Type:

type
  MACDStrategy* = ref object of Strategy
    fastPeriod*: int
    slowPeriod*: int
    signalPeriod*: int
    macdIndicator*: MACD
    lastMACDAbove*: bool

Strategy Behavior:

MACD captures momentum shifts by comparing two exponential moving averages. The MACD line (difference between fast and slow EMAs) represents momentum direction and strength. The signal line (EMA of MACD) smooths these movements. Crossovers indicate momentum changes.

Compared to Crossover Strategy:

While moving average crossovers directly compare price averages, MACD compares the difference between averages to a smoothed version of that difference. This makes MACD more responsive to acceleration and deceleration in price movement.

Standard Parameters:

The 12/26/9 combination was developed for daily stock charts in the 1970s. These parameters remain widely used, but different assets and timeframes may benefit from adjustment.

Signal Generation:

Like crossover strategies, MACD generates discrete signals at crossover points. The lastMACDAbove field prevents signal repetition.

Example:

import tzutrader

# Standard MACD
let standard = newMACDStrategy()

# Faster MACD (more responsive)
let fast = newMACDStrategy(fastPeriod = 8, slowPeriod = 17, signalPeriod = 9)

let data = readCSV("data/AAPL.csv")
let report = quickBacktest("AAPL", standard, data)

echo report

Bollinger Bands Strategy

Mean reversion strategy using Bollinger Bands volatility envelopes.

Trading Logic: - Buy: Price touches or falls below lower band - Sell: Price touches or exceeds upper band - Stay: Price within bands

Constructor:

proc newBollingerStrategy*(period: int = 20, stdDev: float64 = 2.0,
                           symbol: string = ""): BollingerStrategy

Parameters:

Parameter Type Default Description Typical Range
period int 20 SMA period for middle band 10-50
stdDev float64 2.0 Standard deviations for bands 1.5-2.5
symbol string "" Target symbol

Type:

type
  BollingerStrategy* = ref object of Strategy
    period*: int
    stdDev*: float64
    lastPosition*: Position

Strategy Behavior:

Bollinger Bands create a volatility-adjusted envelope around a moving average. When price reaches the outer bands, it's statistically "far" from the mean and likely to revert. The bands expand during volatile periods and contract during quiet periods, automatically adjusting to market conditions.

Statistical Interpretation:

With 2 standard deviations, approximately 95% of price observations should fall within the bands. When price breaches a band, it's an outlier event—the strategy bets on regression to the mean.

Volatility Adaptation:

Unlike RSI thresholds (which are fixed numbers), Bollinger Bands adapt to the stock's current volatility. A volatile stock gets wider bands, a stable stock gets narrower bands. This makes the strategy work across different assets without parameter tuning.

Example:

import tzutrader

# Standard Bollinger (95% confidence)
let standard = newBollingerStrategy(period = 20, stdDev = 2.0)

# Tighter bands (trade more often, less extreme moves)
let tight = newBollingerStrategy(period = 20, stdDev = 1.5)

# Wider bands (wait for very extreme moves)
let wide = newBollingerStrategy(period = 20, stdDev = 2.5)

let data = readCSV("data/AAPL.csv")

for strategy in [standard, tight, wide]:
  let report = quickBacktest("AAPL", strategy, data)
  echo strategy.name, ": ", report.totalTrades, " trades"

Building Custom Strategies

Create custom strategies by inheriting from Strategy and implementing the required methods.

Basic Structure

import tzutrader

type
  MyStrategy* = ref object of Strategy
    # Your strategy-specific fields
    myParameter*: float64
    myIndicator*: SMA

proc newMyStrategy*(myParameter: float64): MyStrategy =
  result = MyStrategy(
    name: "My Custom Strategy",
    myParameter: myParameter,
    myIndicator: newSMA(20)
  )

method onBar*(s: MyStrategy, bar: OHLCV): Signal =
  # Update indicators and check conditions
  let smaVal = s.myIndicator.update(bar.close)

  var position = Stay
  if not smaVal.isNaN:
    if bar.close > smaVal * 1.02:
      position = Buy
    elif bar.close < smaVal * 0.98:
      position = Sell

  result = Signal(
    position: position,
    symbol: s.symbol,
    timestamp: bar.timestamp,
    price: bar.close,
    reason: "Price vs SMA"
  )

Example: Dual RSI Strategy

A custom strategy using two RSI periods for confirmation:

import tzutrader

type
  DualRSIStrategy* = ref object of Strategy
    shortRSI*: RSI
    longRSI*: RSI
    oversold*: float64
    overbought*: float64

proc newDualRSIStrategy*(shortPeriod: int = 7, longPeriod: int = 21,
                         oversold: float64 = 30.0, 
                         overbought: float64 = 70.0): DualRSIStrategy =
  result = DualRSIStrategy(
    name: "Dual RSI Strategy",
    shortRSI: newRSI(shortPeriod),
    longRSI: newRSI(longPeriod),
    oversold: oversold,
    overbought: overbought
  )

method onBar*(s: DualRSIStrategy, bar: OHLCV): Signal =
  # Update both RSI indicators
  let shortVal = s.shortRSI.update(bar.open, bar.close)
  let longVal = s.longRSI.update(bar.open, bar.close)

  var position = Stay
  var reason = ""

  if not shortVal.isNaN and not longVal.isNaN:
    # Buy when BOTH RSIs are oversold
    if shortVal < s.oversold and longVal < s.oversold:
      position = Buy
      reason = &"Both RSIs oversold: {shortVal:.1f}, {longVal:.1f}"
    # Sell when BOTH RSIs are overbought
    elif shortVal > s.overbought and longVal > s.overbought:
      position = Sell
      reason = &"Both RSIs overbought: {shortVal:.1f}, {longVal:.1f}"

  result = Signal(
    position: position,
    symbol: s.symbol,
    timestamp: bar.timestamp,
    price: bar.close,
    reason: reason
  )

# Usage
let strategy = newDualRSIStrategy()
let data = readCSV("data/AAPL.csv")

for bar in data:
  let signal = strategy.onBar(bar)
  if signal.position != Stay:
    echo signal.reason

Custom Strategy Guidelines

Keep it simple: Complex strategies with many parameters often overfit historical data and fail in live trading.

Handle NaN values: Indicators return NaN when insufficient data exists. Check for NaN before making decisions.

Provide reasons: The reason field helps debug strategy behavior and understand why signals were generated.

Use streaming indicators: Create indicator instances in your strategy and update them in onBar(). Don't reimplement indicator logic.

State management: Indicators maintain their own state internally. No manual state management needed.

Signal Objects

Strategies return Signal objects describing what action to take:

type
  Signal* = object
    position*: Position  # Buy, Sell, or Stay
    symbol*: string      # Target symbol
    timestamp*: int64    # When signal generated
    price*: float64      # Price at signal time
    reason*: string      # Human-readable explanation

See Core Types Reference for complete Signal specification.

Signal interpretation:

  • Buy: Enter or add to a long position
  • Sell: Exit or reduce a long position (not short selling)
  • Stay: No action, hold current state

Performance Considerations

Streaming architecture benefits:

The streaming-only design provides: - O(1) memory: Constant memory usage regardless of data size - O(1) updates: Each bar processed in constant time - Live trading ready: Same code for backtesting and production - No reprocessing: State maintained across updates

Memory usage:

Indicators use fixed-size circular buffers. Total memory per strategy is typically < 10KB regardless of how much data is processed.

Common Strategy Patterns

Combining Indicators

# Buy when RSI oversold AND price below lower Bollinger Band
if rsiVal < 30.0 and price <= lowerBand:
  position = Buy

Confirmation Logic

# Buy when MACD crossover confirmed by volume
if macdCrossed and volume > avgVolume * 1.5:
  position = Buy

Exit Conditions

# Exit when profit target reached or stop loss hit
if unrealizedPnL > profitTarget or unrealizedPnL < -stopLoss:
  position = Sell

Position Tracking

# Only enter new positions, don't pyramid
if lastSignal != Buy and rsiVal < oversold:
  position = Buy

See Also

Strategy Category Guides

Other References