← Back to All Tutorials

Tutorial 06: MACD 5-Minute Backtest Beginner-Friendly

Table of Contents
  1. What Does This Script Do?
  2. Key Concepts
  3. How It Works
  4. Code Walkthrough
  5. The Math: MACD Explained
  6. Dependencies
  7. Glossary

1. What Does This Script Do?

This is a backtest-only script (no live trading). It tests whether the MACD technical indicator can predict BTC's direction in 5-minute windows, simulating how the strategy would have performed on Polymarket's binary options.

Simple Analogy: Imagine you have a weather prediction method and 2 years of weather data. A backtest is like going through all that historical data and checking: "If I had used my method every day, how often would I have been right?" This script does exactly that for BTC price prediction using MACD.
Important: This is a backtest - it tests the strategy on historical data. Past performance does not guarantee future results. A strategy that worked historically may fail in live trading.

2. Key Concepts

What is MACD?

Moving Average Convergence Divergence (MACD) is a momentum indicator that shows the relationship between two moving averages of an asset's price.

When MACD > Signal: Momentum is bullish (price likely going up) -> predict UP

When MACD < Signal: Momentum is bearish (price likely going down) -> predict DOWN

What is an EMA?

An Exponential Moving Average is a type of average that gives more weight to recent prices. It reacts faster to price changes than a simple moving average. The "12-period EMA" averages the last 12 data points, weighting recent ones more heavily.

OHLCV Data

Open, High, Low, Close, Volume - the standard way to represent price data. Each "candle" shows where the price opened, the highest it went, the lowest it went, where it closed, and how much was traded.

3. How It Works

1. Load 1-min
BTC/USD Data


2. Compute MACD
(12/26/9)


3. Group into
5-min Windows


4. Generate Signals:
MACD > Signal = UP


5. Simulate P&L with
Polymarket Payouts


6. Run Optimization
over MACD Combos

4. Code Walkthrough

1 build_5min_markets() - Create Market Windows

def build_5min_markets(df):
    # Group every 5 one-minute candles into one 5-minute market
    df['market_start'] = df['datetime'].dt.floor('5min')

    markets = df.groupby('market_start').agg(
        market_open=('open', 'first'),    # First candle's open
        market_high=('high', 'max'),      # Highest point
        market_low=('low', 'min'),        # Lowest point
        market_close=('close', 'last'),    # Last candle's close
        candle_count=('close', 'count'),  # Must be exactly 5
        macd_at_open=('macd_line', 'first'),  # MACD at market open
        signal_at_open=('macd_signal', 'first'),
    ).reset_index()

    # Only keep complete 5-candle windows
    markets = markets[markets['candle_count'] == 5]

    # Label: UP if close >= open, else DOWN
    markets['actual_direction'] = np.where(
        markets['market_close'] >= markets['market_open'], 'UP', 'DOWN'
    )

What it does: Takes 1-minute candle data and groups it into 5-minute windows (exactly like Polymarket's markets). For each window, it records the opening price, highest price, lowest price, closing price, and the MACD value at the start. Then it labels each window as UP or DOWN based on whether price went up or down.

2 generate_macd_signals() - Make Predictions

# Simple rule: MACD > Signal line -> predict UP
#               MACD < Signal line -> predict DOWN
markets['prediction'] = np.where(
    markets['macd_at_open'] >= markets['signal_at_open'],
    'UP', 'DOWN'
)

What it does: At the start of each 5-minute window, checks if the MACD line is above the Signal line. If MACD > Signal, it predicts UP. Otherwise, DOWN. This is the simplest MACD trading rule.

3 simulate_pnl() - Calculate Profits

Simulates what would happen if you bet on each 5-minute market using Polymarket's payout structure:

4 run_optimization() - Test Multiple MACD Settings

Tests different MACD parameter combinations (e.g., 12/26/9, 6/20/5, 8/21/5) to find which settings work best. This is called parameter optimization.

5. The Math: MACD Explained Visually

Price:     100  102  101  103  105  104  106  108  107  109
                              |
Fast EMA (12):     Responds quickly to recent prices
Slow EMA (26):     Smoother, slower to react
                              |
MACD Line = Fast EMA - Slow EMA
  Positive = price accelerating upward
  Negative = price accelerating downward
                              |
Signal Line = 9-period EMA of MACD Line
  Smooths out the MACD Line
                              |
When MACD crosses ABOVE Signal -> Buy signal (bullish)
When MACD crosses BELOW Signal -> Sell signal (bearish)
        

6. Dependencies

pip install pandas pandas_ta numpy

Note: This script requires a BTC/USD 1-minute OHLCV data file (CSV). No API keys or live exchange connections needed - it's purely historical analysis.

7. Glossary

TermMeaning
BacktestTesting a strategy on historical data to see how it would have performed
MACDMoving Average Convergence Divergence - a momentum indicator
EMAExponential Moving Average - weighted average favoring recent prices
Signal LineA smoothed version of the MACD line used to generate buy/sell signals
HistogramThe difference between MACD and Signal, shown as bars
CrossoverWhen MACD crosses above or below the Signal line
Win RatePercentage of trades that were profitable
EdgeThe advantage over random guessing (above 50% or the breakeven rate)
Parameter OptimizationTesting different settings to find the best combination

8. Full Code: Python to Pseudo-Code Translation

load_data() - Load Historical Price Data

# --- PYTHON ---
def load_data():
    df = pd.read_csv(DATA_PATH, parse_dates=['datetime'])
    df = df.sort_values('datetime').reset_index(drop=True)
    return df

# --- PSEUDO-CODE ---
FUNCTION load_data():
    READ the CSV file containing 1-minute BTC/USD price data
    PARSE the 'datetime' column as actual dates (not just text)
    SORT all rows chronologically (oldest first)
    RESET the row index so it goes 0, 1, 2, 3...
    RETURN the dataframe

compute_macd() - Calculate MACD Indicator

# --- PYTHON ---
def compute_macd(df):
    macd_result = ta.macd(df['close'], fast=12, slow=26, signal=9)
    df['macd_line'] = macd_result['MACD_12_26_9']
    df['macd_signal'] = macd_result['MACDs_12_26_9']
    df['macd_histogram'] = macd_result['MACDh_12_26_9']
    return df

# --- PSEUDO-CODE ---
FUNCTION compute_macd(dataframe):
    FOR each row, using the closing price:
        CALCULATE the 12-period Exponential Moving Average (fast EMA)
        CALCULATE the 26-period Exponential Moving Average (slow EMA)
        MACD Line = fast EMA minus slow EMA
        Signal Line = 9-period EMA of the MACD Line
        Histogram = MACD Line minus Signal Line

    ADD three new columns to the dataframe:
        "macd_line" = the MACD values
        "macd_signal" = the Signal line values
        "macd_histogram" = the Histogram values

    RETURN the updated dataframe

build_5min_markets() - Group Into 5-Minute Windows

# --- PYTHON ---
def build_5min_markets(df):
    df['market_start'] = df['datetime'].dt.floor('5min')
    markets = df.groupby('market_start').agg(
        market_open=('open', 'first'), market_high=('high', 'max'),
        market_low=('low', 'min'), market_close=('close', 'last'),
        candle_count=('close', 'count'),
        macd_at_open=('macd_line', 'first'),
        signal_at_open=('macd_signal', 'first'),
    ).reset_index()
    markets = markets[markets['candle_count'] == 5]
    markets['actual_direction'] = np.where(
        markets['market_close'] >= markets['market_open'], 'UP', 'DOWN')

# --- PSEUDO-CODE ---
FUNCTION build_5min_markets(dataframe):
    ROUND each timestamp down to the nearest 5-minute mark
        (e.g. 10:03 becomes 10:00, 10:07 becomes 10:05)

    GROUP all 1-minute candles by their 5-minute window:
        market_open = the OPEN price of the FIRST candle
        market_high = the HIGHEST price across all 5 candles
        market_low = the LOWEST price across all 5 candles
        market_close = the CLOSE price of the LAST candle
        candle_count = how many candles in this group
        macd_at_open = the MACD value at the start of the window

    KEEP ONLY windows that have exactly 5 candles (complete windows)

    FOR each 5-minute window:
        IF the closing price >= opening price: label it "UP"
        IF the closing price < opening price: label it "DOWN"

generate_macd_signals() - Make Predictions

# --- PYTHON ---
def generate_macd_signals(markets):
    markets['macd_pick'] = np.where(
        markets['macd_at_open'] > markets['signal_at_open'], 'UP', 'DOWN')
    markets['win'] = markets['macd_pick'] == markets['actual_direction']

# --- PSEUDO-CODE ---
FUNCTION generate_macd_signals(markets dataframe):
    FOR each 5-minute market:
        IF MACD line > Signal line at market open:
            PREDICT "UP"
        ELSE:
            PREDICT "DOWN"

    COMPARE each prediction to the actual outcome:
        IF prediction matches reality: mark as WIN
        IF prediction is wrong: mark as LOSS

simulate_pnl() - Calculate Simulated Profits

# --- PYTHON ---
def simulate_pnl(markets):
    shares_per_bet = USD_PER_BET / ENTRY_PRICE    # 10 / 0.54 = ~18.5 shares
    win_profit = (1.0 - ENTRY_PRICE) * shares_per_bet   # ~$8.52
    loss_amount = ENTRY_PRICE * shares_per_bet            # ~$10.00
    markets['pnl'] = np.where(markets['win'], win_profit, -loss_amount)
    markets['cumulative_pnl'] = markets['pnl'].cumsum()
    return markets, shares_per_bet, win_profit, loss_amount

# --- PSEUDO-CODE ---
FUNCTION simulate_pnl(markets):
    CALCULATE shares per bet = $10 / $0.54 = about 18.5 shares
    IF we win: profit = ($1.00 - $0.54) x 18.5 = about $8.52 per bet
    IF we lose: loss = $0.54 x 18.5 = about $10.00 per bet

    FOR each market:
        IF we won: ADD $8.52 to running P&L
        IF we lost: SUBTRACT $10.00 from running P&L

    CREATE a cumulative P&L column (running total over time)
    RETURN the updated data

main() - Run the Complete Backtest

# --- PYTHON ---
def main():
    df = load_data()
    df = compute_macd(df)
    markets = build_5min_markets(df)
    markets = generate_macd_signals(markets)
    markets, shares, win_profit, loss_amount = simulate_pnl(markets)
    total_pnl, win_rate, edge = print_results(markets, shares, win_profit, loss_amount)
    save_results(markets)
    run_optimization(df)

# --- PSEUDO-CODE ---
FUNCTION main():
    STEP 1: Load the 1-minute BTC price data from CSV
    STEP 2: Calculate MACD for every 1-minute candle
    STEP 3: Group into 5-minute windows (simulating Polymarket markets)
    STEP 4: Generate predictions (MACD > Signal = UP, else DOWN)
    STEP 5: Simulate profit/loss using Polymarket payout math
    STEP 6: Print all results (win rate, total P&L, edge over breakeven)
    STEP 7: Save detailed results to a CSV file
    STEP 8: Run parameter optimization testing different MACD settings