Skip to content

Writing Your First Trading Strategy

Learn to create a custom trading strategy from scratch. We'll build a simple but effective strategy that you can expand and customize.

Strategy Anatomy 101

Every strategy has three main components:

python
def strategy(data_contexts, portfolio, state):
    # 1. Get market data
    # 2. Make trading decision  
    # 3. Return order (or None)
    return order

Let's break this down:

  • data_contexts: Market prices and indicators for each symbol
  • portfolio: Your account balance, positions, and performance
  • state: Memory between function calls (optional)

Your First Strategy: Buy and Hold

Let's start with the simplest possible strategy:

python
def strategy(data_contexts, portfolio, state):
    """Buy and hold strategy - buy once and never sell"""
    
    # Get Apple's data
    aapl = data_contexts['AAPL']
    
    # Check if we already own shares
    current_position = portfolio.position('AAPL')
    
    # If we don't own any, buy 100 shares
    if current_position == 0:
        return {
            'symbol': 'AAPL',
            'action': 'buy',
            'quantity': 100
        }
    
    # Otherwise, do nothing
    return None

What this does:

  1. Checks if we own AAPL shares
  2. If not, buys 100 shares
  3. Holds forever (never sells)

Level 2: Adding a Sell Condition

Let's add profit-taking logic:

python
def strategy(data_contexts, portfolio, state):
    """Buy low, sell high - take profits at 10% gain"""
    
    aapl = data_contexts['AAPL']
    current_position = portfolio.position('AAPL')
    current_price = aapl.close
    
    # Initialize state to track entry price
    if 'entry_price' not in state:
        state['entry_price'] = None
    
    # Buy logic: if we don't have a position
    if current_position == 0:
        state['entry_price'] = current_price
        return {
            'symbol': 'AAPL',
            'action': 'buy',
            'quantity': 100
        }
    
    # Sell logic: if we made 10% profit
    if current_position > 0 and state['entry_price']:
        profit_percent = (current_price - state['entry_price']) / state['entry_price']
        
        if profit_percent >= 0.10:  # 10% profit
            print(f"Taking profits! Gain: {profit_percent:.2%}")
            state['entry_price'] = None
            return {
                'symbol': 'AAPL',
                'action': 'sell',
                'quantity': 'all'
            }
    
    return None

New concepts:

  • Using state to remember our entry price
  • Calculating profit percentage
  • Selling entire position with quantity: 'all'
  • Debug output with print()

Level 3: Using Technical Indicators

Now let's use moving averages for smarter entries:

python
def strategy(data_contexts, portfolio, state):
    """Trend following - buy on uptrend, sell on downtrend"""
    
    aapl = data_contexts['AAPL']
    
    # Calculate indicators
    sma_fast = aapl.sma(20)   # 20-day moving average
    sma_slow = aapl.sma(50)   # 50-day moving average
    current_price = aapl.close
    
    # Skip if indicators aren't ready (need 50 days of data)
    if sma_slow != sma_slow:  # Check for NaN
        return None
    
    current_position = portfolio.position('AAPL')
    
    # BUY SIGNAL: Fast MA crosses above Slow MA (uptrend starting)
    if sma_fast > sma_slow and current_position == 0:
        print(f"Uptrend detected! SMA20={sma_fast:.2f} > SMA50={sma_slow:.2f}")
        return {
            'symbol': 'AAPL',
            'action': 'buy',
            'quantity': 100
        }
    
    # SELL SIGNAL: Fast MA crosses below Slow MA (downtrend starting)
    elif sma_fast < sma_slow and current_position > 0:
        print(f"Downtrend detected! SMA20={sma_fast:.2f} < SMA50={sma_slow:.2f}")
        return {
            'symbol': 'AAPL',
            'action': 'sell',
            'quantity': 'all'
        }
    
    return None

Technical indicators available:

  • sma(period) - Simple Moving Average
  • ema(period) - Exponential Moving Average
  • rsi(period) - Relative Strength Index (0-100)

Level 4: Risk Management

Add stop-loss to limit losses:

python
def strategy(data_contexts, portfolio, state):
    """Trend following with risk management"""
    
    aapl = data_contexts['AAPL']
    
    # Indicators
    sma_20 = aapl.sma(20)
    sma_50 = aapl.sma(50)
    current_price = aapl.close
    
    # Skip if indicators aren't ready
    if sma_50 != sma_50:
        return None
    
    # Initialize state
    if 'highest_price' not in state:
        state['highest_price'] = 0
        state['entry_price'] = 0
    
    current_position = portfolio.position('AAPL')
    
    # Entry logic
    if sma_20 > sma_50 and current_position == 0:
        state['entry_price'] = current_price
        state['highest_price'] = current_price
        return {
            'symbol': 'AAPL',
            'action': 'buy',
            'quantity': 100
        }
    
    # Exit logic (when holding position)
    if current_position > 0:
        # Update highest price (for trailing stop)
        state['highest_price'] = max(state['highest_price'], current_price)
        
        # Stop loss: Exit if price drops 5% from entry
        loss_from_entry = (current_price - state['entry_price']) / state['entry_price']
        if loss_from_entry <= -0.05:
            print(f"Stop loss triggered! Loss: {loss_from_entry:.2%}")
            return {'symbol': 'AAPL', 'action': 'sell', 'quantity': 'all'}
        
        # Trailing stop: Exit if price drops 8% from highest
        drop_from_high = (current_price - state['highest_price']) / state['highest_price']
        if drop_from_high <= -0.08:
            print(f"Trailing stop triggered! Drop from high: {drop_from_high:.2%}")
            return {'symbol': 'AAPL', 'action': 'sell', 'quantity': 'all'}
        
        # Trend exit: Sell if trend reverses
        if sma_20 < sma_50:
            print("Trend reversal - exiting position")
            return {'symbol': 'AAPL', 'action': 'sell', 'quantity': 'all'}
    
    return None

Risk management concepts:

  • Stop Loss: Limit losses to 5% of entry price
  • Trailing Stop: Lock in profits by selling if price drops 8% from peak
  • Trend Exit: Sell when moving averages signal downtrend

Level 5: Position Sizing

Use portfolio percentage instead of fixed shares:

python
def strategy(data_contexts, portfolio, state):
    """Dynamic position sizing based on portfolio value"""
    
    aapl = data_contexts['AAPL']
    
    # Indicators
    sma_20 = aapl.sma(20)
    rsi = aapl.rsi(14)
    current_price = aapl.close
    
    # Skip if indicators aren't ready
    if sma_20 != sma_20 or rsi != rsi:
        return None
    
    current_position = portfolio.position('AAPL')
    
    # Buy signal: Uptrend + Not overbought
    if current_position == 0:
        if sma_20 > aapl.sma(50) and rsi < 70:
            # Calculate position size as 20% of portfolio
            position_value = portfolio.equity * 0.20
            shares_to_buy = int(position_value / current_price)
            
            if shares_to_buy > 0:
                print(f"Buying {shares_to_buy} shares (20% of portfolio)")
                return {
                    'symbol': 'AAPL',
                    'action': 'buy',
                    'quantity': shares_to_buy
                }
    
    # Sell signal: Overbought or downtrend
    elif current_position > 0:
        if rsi > 80 or sma_20 < aapl.sma(50):
            reason = "RSI overbought" if rsi > 80 else "Trend reversal"
            print(f"Selling: {reason}")
            return {
                'symbol': 'AAPL',
                'action': 'sell',
                'quantity': 'all'
            }
    
    return None

Complete Example: Momentum Strategy

Here's a complete strategy combining everything we've learned:

python
def strategy(data_contexts, portfolio, state):
    """
    Momentum Trading Strategy
    - Entry: SMA crossover + RSI confirmation
    - Exit: Stop loss, trailing stop, or signal reversal
    - Position sizing: Risk 2% per trade
    """
    
    # Configuration
    RISK_PER_TRADE = 0.02  # Risk 2% of portfolio per trade
    STOP_LOSS_PCT = 0.05   # 5% stop loss
    TRAILING_STOP_PCT = 0.08  # 8% trailing stop
    
    # Get data
    aapl = data_contexts['AAPL']
    
    # Calculate indicators
    sma_fast = aapl.sma(10)
    sma_slow = aapl.sma(30)
    rsi = aapl.rsi(14)
    current_price = aapl.close
    volume = aapl.volume
    avg_volume = sum(aapl.history('volume', 20)) / 20
    
    # Check if indicators are ready
    if sma_slow != sma_slow or rsi != rsi:
        return None
    
    # Initialize state
    if 'trades_count' not in state:
        state['trades_count'] = 0
        state['entry_price'] = 0
        state['highest_price'] = 0
    
    current_position = portfolio.position('AAPL')
    
    # ENTRY LOGIC
    if current_position == 0:
        # Entry conditions:
        # 1. Moving average crossover (trend)
        # 2. RSI not overbought (momentum)
        # 3. Volume above average (confirmation)
        
        trend_signal = sma_fast > sma_slow
        momentum_signal = 30 < rsi < 70
        volume_signal = volume > avg_volume
        
        if trend_signal and momentum_signal and volume_signal:
            # Calculate position size based on risk
            account_risk = portfolio.equity * RISK_PER_TRADE
            risk_per_share = current_price * STOP_LOSS_PCT
            shares_to_buy = int(account_risk / risk_per_share)
            
            # Ensure we don't use more than 30% of portfolio
            max_shares = int((portfolio.equity * 0.30) / current_price)
            shares_to_buy = min(shares_to_buy, max_shares)
            
            if shares_to_buy > 0 and portfolio.cash > shares_to_buy * current_price:
                state['entry_price'] = current_price
                state['highest_price'] = current_price
                state['trades_count'] += 1
                
                print(f"Trade #{state['trades_count']}: BUY {shares_to_buy} shares at ${current_price:.2f}")
                print(f"  Signals: SMA={sma_fast:.2f}/{sma_slow:.2f}, RSI={rsi:.1f}, Vol={volume/avg_volume:.1f}x")
                
                return {
                    'symbol': 'AAPL',
                    'action': 'buy',
                    'quantity': shares_to_buy
                }
    
    # EXIT LOGIC
    elif current_position > 0:
        # Update trailing stop
        state['highest_price'] = max(state['highest_price'], current_price)
        
        # Calculate performance
        unrealized_pnl = (current_price - state['entry_price']) / state['entry_price']
        drop_from_high = (current_price - state['highest_price']) / state['highest_price']
        
        # Exit conditions (in priority order):
        
        # 1. Stop Loss
        if unrealized_pnl <= -STOP_LOSS_PCT:
            print(f"STOP LOSS: Exiting at ${current_price:.2f} (Loss: {unrealized_pnl:.2%})")
            return {'symbol': 'AAPL', 'action': 'sell', 'quantity': 'all'}
        
        # 2. Trailing Stop
        if drop_from_high <= -TRAILING_STOP_PCT and unrealized_pnl > 0:
            print(f"TRAILING STOP: Locking profits at ${current_price:.2f} (Gain: {unrealized_pnl:.2%})")
            return {'symbol': 'AAPL', 'action': 'sell', 'quantity': 'all'}
        
        # 3. Signal Reversal
        if sma_fast < sma_slow or rsi > 80:
            reason = "Overbought (RSI>80)" if rsi > 80 else "Trend reversal"
            print(f"SIGNAL EXIT ({reason}): Selling at ${current_price:.2f} (P&L: {unrealized_pnl:.2%})")
            return {'symbol': 'AAPL', 'action': 'sell', 'quantity': 'all'}
    
    return None

Testing Your Strategy

  1. Copy the code into the strategy editor
  2. Click Run Backtest or press Shift+Enter
  3. Check Debug Output for your print statements
  4. Analyze Results:
    • Did it make money?
    • How many trades?
    • What was the max drawdown?

Common Patterns to Try

Pattern 1: Mean Reversion

Buy when RSI < 30 (oversold), sell when RSI > 70 (overbought)

Pattern 2: Breakout

Buy when price breaks above 20-day high, sell at 10-day low

Pattern 3: Volume Surge

Buy when volume is 2x average and price is rising

Debugging Tips

Add print statements to understand your strategy:

python
print(f"Price: ${current_price:.2f}, SMA: {sma:.2f}, RSI: {rsi:.1f}")
print(f"Position: {current_position} shares, Cash: ${portfolio.cash:.2f}")
print(f"Signal triggered: Buying {shares} shares")

Next Steps

You're ready to:


Remember: Backtesting shows historical performance, not future results. Always test thoroughly before using real money!

Test your trading strategies risk-free with professional backtesting.