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.
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
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.
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.
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.
# 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.
Simulates what would happen if you bet on each 5-minute market using Polymarket's payout structure:
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.
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)
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.
| Term | Meaning |
|---|---|
| Backtest | Testing a strategy on historical data to see how it would have performed |
| MACD | Moving Average Convergence Divergence - a momentum indicator |
| EMA | Exponential Moving Average - weighted average favoring recent prices |
| Signal Line | A smoothed version of the MACD line used to generate buy/sell signals |
| Histogram | The difference between MACD and Signal, shown as bars |
| Crossover | When MACD crosses above or below the Signal line |
| Win Rate | Percentage of trades that were profitable |
| Edge | The advantage over random guessing (above 50% or the breakeven rate) |
| Parameter Optimization | Testing different settings to find the best combination |
# --- 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
# --- 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
# --- 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"
# --- 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
# --- 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
# --- 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