Understanding Bitcoin’s 4-year cycles through data-driven analysis and systematic trading approaches.
Introduction
Bitcoin’s price history has been characterized by dramatic boom-and-bust cycles that align remarkably well with its programmed halving events. As quantitative traders, we’re interested in understanding these patterns not through speculation, but through rigorous data analysis.
In this post, we’ll dissect Bitcoin’s halving cycles, analyze historical price behavior, and examine where we are in the current cycle. As of December 13, 2025, we’re approximately 20 months post-halving and 2 months past the cycle peakof $126,210 reached in October 2025. This gives us an excellent opportunity to analyze how this cycle compared to historical patterns and what systematic traders should consider for 2026. We’ll use Python to extract insights from on-chain data and historical price movements.
The Halving Mechanism: Understanding the Supply Shock
Bitcoin’s protocol includes a deflationary mechanism: every 210,000 blocks (approximately 4 years), the block reward miners receive is cut in half. This creates a predictable supply schedule:
Genesis to Nov 2012: 50 BTC per block
2012-2016: 25 BTC per block
2016-2020: 12.5 BTC per block
2020-2024: 6.25 BTC per block
2024-2028: 3.125 BTC per block (current)
The most recent halving occurred on April 20, 2024, reducing daily issuance from ~900 BTC to ~450 BTC. This represents a significant supply shock in an asset with growing institutional demand.
Historical Cycle Analysis
Let’s examine the performance characteristics of each halving cycle:
Cycle 1: 2012 Halving
Pre-halving price: ~$12
Peak: ~$1,200 (November 2013)
Days to peak: ~373 days
Return from halving: ~10,000%
Characteristics: Retail-driven, high volatility, nascent market
Cycle 2: 2016 Halving
Pre-halving price: ~$650
Peak: ~$19,800 (December 2017)
Days to peak: ~525 days
Return from halving: ~3,000%
Characteristics: ICO boom, retail FOMO, limited institutional presence
Cycle 3: 2020 Halving
Pre-halving price: ~$8,600
Peak: ~$69,000 (November 2021)
Days to peak: ~546 days
Return from halving: ~700%
Characteristics: COVID stimulus, institutional adoption begins, narrative shift to “digital gold”
Cycle 4: 2024 Halving (Current)
Halving price: ~$64,000 (April 20, 2024)
Peak: ~$126,210 (October 6, 2025)
Days to peak: ~534 days
Return from halving: ~97%
Current price: ~$90,000 (December 13, 2025)
Notable differences: Pre-halving ATH, spot ETF approval, strong institutional adoption
Key Observation: The 2024 cycle peak occurred at 534 days - right in the historical 12-18 month window (365-547 days). Returns continued to diminish as expected, but the pattern held remarkably well.
Python Analysis: Cycle Patterns
Let’s write code to analyze Bitcoin’s historical cycle behavior:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
import yfinance as yf
# Define halving dates
halving_dates = {
'Halving 1': '2012-11-28',
'Halving 2': '2016-07-09',
'Halving 3': '2020-05-11',
'Halving 4': '2024-04-20'
}
# Define cycle peaks
cycle_peaks = {
'Halving 1': ('2013-11-30', 373), # (date, days from halving)
'Halving 2': ('2017-12-17', 525),
'Halving 3': ('2021-11-10', 546),
'Halving 4': ('2025-10-06', 534)
}
# Fetch Bitcoin price data (data available from 2012-11-28)
btc = yf.download('BTC-USD', start='2012-11-28', end='2025-12-13', auto_adjust=False)
# Handle MultiIndex columns by flattening them if needed
if isinstance(btc.columns, pd.MultiIndex):
btc.columns = btc.columns.droplevel(1)
# Extract Close column and ensure it's a DataFrame with 'Close' column
if 'Close' in btc.columns:
close_series = btc['Close']
if isinstance(close_series, pd.Series):
btc = close_series.to_frame(name='Close')
else:
# If it's a DataFrame, take the first column
btc = pd.DataFrame({'Close': close_series.iloc[:, 0]}, index=btc.index)
else:
# If Close column doesn't exist, use first column
btc = btc.iloc[:, [0]].rename(columns={btc.columns[0]: 'Close'})
# Function to analyze cycle performance
def analyze_cycle(halving_date, days_before=365, days_after=700):
"""
Analyze price performance around halving events
days_after=700 to capture full cycle including post-peak
"""
halving = pd.to_datetime(halving_date)
# Get date ranges, but use available data start if before halving date
data_start = btc.index.min()
requested_start = halving - timedelta(days=days_before)
start_date = max(data_start, requested_start)
end_date = halving + timedelta(days=days_after)
# Filter data
cycle_data = btc[(btc.index >= start_date) & (btc.index <= end_date)].copy()
if len(cycle_data) == 0:
return None
# Normalize to halving day = 0
cycle_data['Days_from_halving'] = (cycle_data.index - halving).days
# Calculate returns
# Find the nearest date to halving date in cycle_data
nearest_idx = cycle_data.index.get_indexer([halving], method='nearest')[0]
halving_price = cycle_data.iloc[nearest_idx]['Close']
cycle_data['Return_from_halving'] = (cycle_data['Close'] / halving_price - 1) * 100
return cycle_data
# Analyze all cycles
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
axes = axes.flatten()
for idx, (name, date) in enumerate(halving_dates.items()):
cycle_data = analyze_cycle(date)
if cycle_data is not None:
ax = axes[idx]
ax.plot(cycle_data['Days_from_halving'], cycle_data['Return_from_halving'])
ax.axvline(x=0, color='red', linestyle='--', label='Halving Event')
ax.axhline(y=0, color='black', linestyle='-', alpha=0.3)
# Mark peak for each cycle
if name in cycle_peaks:
peak_date, peak_days = cycle_peaks[name]
ax.axvline(x=peak_days, color='darkred', linestyle=':', label=f'Peak ({peak_days}d)')
# Shade historical peak window (12-18 months)
ax.axvspan(365, 547, alpha=0.1, color='red', label='Typical Peak Window')
ax.set_xlabel('Days from Halving')
ax.set_ylabel('Return (%)')
ax.set_title(f'{name}: {date}')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('bitcoin_cycles.png', dpi=300, bbox_inches='tight')
plt.show()
# Calculate key metrics for each cycle
print("\\n=== Cycle Performance Metrics ===\\n")
for name, date in halving_dates.items():
cycle_data = analyze_cycle(date, days_after=700)
if cycle_data is not None and len(cycle_data) > 0:
max_return = cycle_data['Return_from_halving'].max()
days_to_peak = cycle_data.loc[cycle_data['Return_from_halving'].idxmax(), 'Days_from_halving']
print(f"{name} ({date}):")
print(f" Max Return: {max_return:.1f}%")
print(f" Days to Peak: {days_to_peak:.0f}")
print(f" Annualized Return to Peak: {(max_return / days_to_peak * 365):.1f}%")
print()
Unfortunately yfinance has only data up to 2012.11.28
This is a heuristic template, not a validated trading strategy; needs full backtest with costs, drawdowns, and OOS tests.
Quantitative Observations
Running this analysis reveals several key patterns:
1. Diminishing Returns (But Still Substantial)
Each cycle produces lower percentage gains:
Cycle 1: ~10,000% from halving
Cycle 2: ~3,000% from halving
Cycle 3: ~700% from halving
Cycle 4: ~97% from halving (still nearly doubling!)
This follows a power law relationship and is expected as market cap increases.
2. Consistent Peak Timing
Cycle peaks occur remarkably consistently 12-18 months post-halving:
2012: ~373 days to peak
2016: ~525 days to peak
2020: ~546 days to peak
2024: ~534 days to peak ✓
The 2024 cycle peak in October 2025 validated this pattern once again.
3. Drawdown Severity Decreasing
Maximum drawdowns from cycle peaks have been decreasing:
2011-2015: -86.3%
2018: -86.3%
2022: -76.9%
2025: TBD (currently -28% from $126K peak)
This suggests market maturation and stronger support levels.
4. We’re Currently in Late Cycle
As of December 2025:
20 months post-halving (historically late distribution phase)
2 months past ATH (early bear market traditionally)
Trading at $90K vs. $126K peak
Advanced Analysis: On-Chain Metrics
Let’s incorporate on-chain data to better understand cycle positioning:
# Calculate realized price multiple
def calculate_mayer_multiple(btc_price, window=200):
"""
Mayer Multiple = Price / 200-day MA
> 2.4: Historically overbought
< 1.0: Historically oversold
"""
ma_200 = btc_price.rolling(window=window).mean()
mayer_multiple = btc_price / ma_200
return mayer_multiple
# Calculate for current data
btc['MA_200'] = btc['Close'].rolling(window=200).mean()
btc['Mayer_Multiple'] = btc['Close'] / btc['MA_200']
# Current reading (as of Dec 2025)
current_mayer = btc['Mayer_Multiple'].iloc[-1]
print(f"Current Mayer Multiple: {current_mayer:.2f}")
# Historical context during halvings
for name, date in halving_dates.items():
halving_idx = btc.index.get_indexer([pd.to_datetime(date)], method='nearest')[0]
if halving_idx < len(btc):
mayer_at_halving = btc.iloc[halving_idx]['Mayer_Multiple']
print(f"{name}: Mayer Multiple = {mayer_at_halving:.2f}")
# Interpretation
print(f"\\n=== Current Market Assessment ===")
if current_mayer > 2.4:
print("Status: OVERHEATED - Historical sell zone")
elif 2.0 < current_mayer <= 2.4:
print("Status: ELEVATED - Caution warranted")
elif 1.0 <= current_mayer <= 2.0:
print("Status: NEUTRAL - Fair value range")
else:
print("Status: OVERSOLD - Potential accumulation zone")Bitcoin Pi Cycle Top Indicator
def pi_cycle_indicator(btc_price):
"""
Pi Cycle Top: 111-day MA vs 350-day MA * 2
When 111 MA crosses above (350 MA * 2), historically indicates cycle top
"""
ma_111 = btc_price.rolling(window=111).mean()
ma_350 = btc_price.rolling(window=350).mean() * 2
return ma_111, ma_350
btc['MA_111'] = btc['Close'].rolling(window=111).mean()
btc['MA_350_x2'] = btc['Close'].rolling(window=350).mean() * 2
# Check current status
current_111 = btc['MA_111'].iloc[-1]
current_350 = btc['MA_350_x2'].iloc[-1]
distance = ((current_111 / current_350) - 1) * 100
print(f"\\n=== Pi Cycle Indicator ===")
print(f"111-day MA: ${current_111:,.0f}")
print(f"350-day MA x2: ${current_350:,.0f}")
print(f"Distance to top signal: {distance:.1f}%")
if distance > 0:
print("Status: Pi Cycle Top signal TRIGGERED (or recently triggered)")
print("Historical interpretation: Near cycle peak or in distribution")
else:
print("Status: No top signal")The 2024 Cycle: What Made It Different?
The current cycle has several unique characteristics:
1. Pre-Halving All-Time High
Unlike previous cycles, Bitcoin made a new ATH ($73,750 in March 2024) BEFORE the halving. This was driven by:
Spot Bitcoin ETF approvals (January 2024)
Anticipatory institutional positioning
Front-running of the halving event
2. ETF Structural Demand
Spot ETFs created persistent, flow-based demand:
BlackRock’s IBIT: $50B+ AUM
Consistent daily inflows
Reduced available supply on exchanges
3. Market Maturity
Different from previous cycles:
Lower volatility (30-day realized vol ~40% vs. 80%+ in 2017)
Better infrastructure and custody solutions
Institutional investors comprise larger share
Reduced retail dominance
4. Validated Cycle Pattern
Despite unique characteristics, the fundamental cycle pattern held:
Peak at 534 days (within historical 365-547 day range)
Post-halving appreciation
Diminishing returns as expected
Current Position: December 2025
Where Are We Now?
Timeline:
20 months (600+ days) post-halving
2 months past cycle peak ($126,210 in October)
Currently trading around $90,000 (-28% from peak)
Historical Context: At this point in previous cycles:
2012 Cycle (Month 20): Post-peak distribution, entering bear market
2016 Cycle (Month 20): Post-peak correction phase
2020 Cycle (Month 20): Distribution and decline beginning
Typical Pattern: Historically, 20 months post-halving marks the transition from late bull market to early bear market.
Trading Framework for Bitcoin Cycles
Based on our quantitative analysis, here’s a systematic approach:
Position Sizing Based on Cycle Phase
def cycle_position_sizing(days_from_halving, days_from_peak=None, base_position=1.0):
"""
Adjust position size based on typical cycle timing
Args:
days_from_halving: Days since most recent halving
days_from_peak: Days since cycle peak (if known)
base_position: Base allocation (e.g., 1.0 = 100%)
Returns:
Recommended position multiplier
"""
# If we know we're past the peak, use that
if days_from_peak is not None and days_from_peak > 0:
# 0-3 months from peak: reduce but hold
if days_from_peak <= 90:
return base_position * 0.5
# 3-6 months: significant reduction
elif days_from_peak <= 180:
return base_position * 0.3
# 6-12 months: minimal exposure
elif days_from_peak <= 365:
return base_position * 0.2
# 12+ months: bear market, ultra-defensive
else:
return base_position * 0.1
# Otherwise use days from halving
if days_from_halving < 0:
# Pre-halving: moderate position
return base_position * 0.75
elif 0 <= days_from_halving < 200:
# Early cycle (0-6 months): accumulation phase
return base_position * 1.0
elif 200 <= days_from_halving < 400:
# Mid cycle (6-13 months): typical strongest performance
return base_position * 1.25
elif 400 <= days_from_halving < 547:
# Late cycle (13-18 months): approaching historical peaks
return base_position * 0.75
else:
# Post typical peak window: defensive positioning
return base_position * 0.25
# Calculate for current cycle (December 2025)
halving_date = pd.to_datetime('2024-04-20')
peak_date = pd.to_datetime('2025-10-06')
current_date = pd.to_datetime('2025-12-13')
current_days_from_halving = (current_date - halving_date).days
current_days_from_peak = (current_date - peak_date).days
current_sizing = cycle_position_sizing(current_days_from_halving, current_days_from_peak)
print(f"\\n=== Current Cycle Position (Dec 2025) ===")
print(f"Days from halving: {current_days_from_halving}")
print(f"Days from peak: {current_days_from_peak}")
print(f"Recommended position sizing: {current_sizing:.2f}x base")
print(f"Interpretation: {'DEFENSIVE - Late cycle/post-peak' if current_sizing < 0.5 else 'MODERATE'}")Risk Management Overlay
def risk_adjusted_positioning(current_price, ma_200, mayer_multiple):
"""
Adjust position based on technical and valuation metrics
Risk-on conditions:
- Price > 200-day MA
- Mayer Multiple 1.0-2.0
Risk-off conditions:
- Price < 200-day MA
- Mayer Multiple > 2.4
"""
risk_score = 1.0
# Price vs MA-200
if current_price > ma_200:
risk_score *= 1.2
else:
risk_score *= 0.6
# Mayer Multiple
if mayer_multiple < 1.0:
risk_score *= 1.3 # Oversold, increase exposure
elif 1.0 <= mayer_multiple <= 2.0:
risk_score *= 1.0 # Neutral
elif 2.0 < mayer_multiple <= 2.4:
risk_score *= 0.7 # Elevated, reduce exposure
else:
risk_score *= 0.3 # Overheated, minimal exposure
return min(risk_score, 1.5) # Cap at 1.5x
# Current risk assessment (December 2025)
current_price = btc['Close'].iloc[-1]
current_ma200 = btc['MA_200'].iloc[-1]
current_mayer = btc['Mayer_Multiple'].iloc[-1]
risk_multiplier = risk_adjusted_positioning(current_price, current_ma200, current_mayer)
print(f"\\n=== Risk Assessment (Dec 2025) ===")
print(f"Price: ${current_price:,.0f}")
print(f"200-day MA: ${current_ma200:,.0f}")
print(f"Mayer Multiple: {current_mayer:.2f}")
print(f"Risk Multiplier: {risk_multiplier:.2f}x")
# Combined recommendation
final_position = current_sizing * risk_multiplier
print(f"\\n=== FINAL POSITION RECOMMENDATION ===")
print(f"Cycle Multiplier: {current_sizing:.2f}x")
print(f"Risk Multiplier: {risk_multiplier:.2f}x")
print(f"Final Recommended Position: {final_position:.2f}x base allocation")Cycle Expectations: Power Law Model
Bitcoin’s price has historically followed a power law relationship with time. Let’s model this:
from scipy.optimize import curve_fit
# Power law function
def power_law(x, a, b):
return a * np.power(x, b)
# Prepare data: days since genesis (Jan 3, 2009)
genesis_date = pd.to_datetime('2009-01-03')
btc_full = btc.copy()
btc_full['Days_since_genesis'] = (btc_full.index - genesis_date).days
# Fit power law (using log prices to reduce influence of extremes)
mask = btc_full['Close'] > 0
x_data = btc_full.loc[mask, 'Days_since_genesis'].values
y_data = btc_full.loc[mask, 'Close'].values
# Fit
params, _ = curve_fit(power_law, x_data, y_data, p0=[0.001, 5])
a, b = params
print(f"\\n=== Power Law Model ===")
print(f"Formula: Price = {a:.6f} * Days^{b:.2f}")
# Project future prices (from Dec 2025)
current_days_from_genesis = (pd.to_datetime('2025-12-13') - genesis_date).days
future_dates = [
(6, '2026-06'),
(12, '2026-12'),
(18, '2027-06'),
(24, '2028-01') # Approximate next halving
]
print("\\nProjections from December 2025:")
for months, label in future_dates:
days = int(months * 30.44)
future_days = current_days_from_genesis + days
proj_price = power_law(future_days, a, b)
print(f"{label} ({months} months): ${proj_price:,.0f}")Looking Ahead: 2026 and Beyond
Expected Cycle Progression
Based on historical patterns, here’s what to expect:
Q1 2026 (Months 21-23 from halving):
Likely consolidation/distribution phase
Range: $70,000 - $110,000
Position: Defensive (0.2-0.3x)
Q2-Q3 2026 (Months 24-29):
Potential bear market begins
Historical analogs suggest $50,000-$70,000 possible
Begin accumulation for next cycle
Q4 2026 - 2027:
Bear market bottom formation
Accumulation phase for 2028 halving cycle
Target range: $45,000-$65,000
2028:
Next halving in April 2028
Cycle repeats
Systematic Trading Strategy
Here’s a complete systematic approach:
class BitcoinCycleStrategy:
def __init__(self, base_allocation=1.0):
self.base_allocation = base_allocation
self.current_halving = pd.to_datetime('2024-04-20')
self.cycle_peak = pd.to_datetime('2025-10-06')
self.next_halving = pd.to_datetime('2028-04-17') # Approximate
def get_cycle_phase(self, current_date):
"""Determine where we are in the cycle"""
days_from_halving = (current_date - self.current_halving).days
days_from_peak = (current_date - self.cycle_peak).days
if days_from_peak > 0:
if days_from_peak <= 90:
return 'EARLY_POST_PEAK'
elif days_from_peak <= 365:
return 'DISTRIBUTION'
else:
return 'BEAR_MARKET'
elif days_from_halving < 0:
return 'PRE_HALVING'
elif days_from_halving < 200:
return 'EARLY_CYCLE'
elif days_from_halving < 400:
return 'MID_CYCLE'
else:
return 'LATE_CYCLE'
def calculate_position_size(self, current_date, current_price, ma_200, mayer_multiple):
"""Calculate recommended position size"""
phase = self.get_cycle_phase(current_date)
days_from_halving = (current_date - self.current_halving).days
days_from_peak = (current_date - self.cycle_peak).days if current_date > self.cycle_peak else None
# Base cycle multiplier
cycle_mult = cycle_position_sizing(days_from_halving, days_from_peak, self.base_allocation)
# Risk adjustment
risk_mult = risk_adjusted_positioning(current_price, ma_200, mayer_multiple)
# Combined
final_position = cycle_mult * risk_mult
return {
'phase': phase,
'base_allocation': self.base_allocation,
'cycle_multiplier': cycle_mult,
'risk_multiplier': risk_mult,
'final_position': final_position,
'days_from_halving': days_from_halving,
'days_from_peak': days_from_peak if days_from_peak else 'N/A'
}
def generate_signal(self, current_date, current_price, ma_200, mayer_multiple):
"""Generate trading signal"""
position_data = self.calculate_position_size(current_date, current_price, ma_200, mayer_multiple)
# Determine action
if position_data['final_position'] > 0.8:
signal = 'ACCUMULATE'
elif position_data['final_position'] > 0.5:
signal = 'HOLD'
elif position_data['final_position'] > 0.3:
signal = 'REDUCE'
else:
signal = 'DEFENSIVE'
return signal, position_data
# Example usage for current date (December 2025)
strategy = BitcoinCycleStrategy(base_allocation=1.0)
latest_date = pd.to_datetime('2025-12-13')
latest_price = btc['Close'].iloc[-1]
latest_ma200 = btc['MA_200'].iloc[-1]
latest_mayer = btc['Mayer_Multiple'].iloc[-1]
signal, position_data = strategy.generate_signal(
latest_date, latest_price, latest_ma200, latest_mayer
)
print(f"\\n=== STRATEGY SIGNAL (December 2025) ===")
print(f"Current Phase: {position_data['phase']}")
print(f"Signal: {signal}")
print(f"Days from halving: {position_data['days_from_halving']}")
print(f"Days from peak: {position_data['days_from_peak']}")
print(f"Cycle multiplier: {position_data['cycle_multiplier']:.2f}x")
print(f"Risk multiplier: {position_data['risk_multiplier']:.2f}x")
print(f"Recommended position: {position_data['final_position']:.2f}x base allocation")Key Takeaways
Bitcoin’s 4-year cycles are remarkably consistent - The 2024 cycle peaked at 534 days post-halving, right in the historical 365-547 day window
Returns diminish but remain significant - While not 10,000% anymore, ~97% returns in 18 months is still exceptional
We’re currently in late cycle - 20 months post-halving, 2 months past peak - historically this is distribution/early bear phase
Timing still matters more than perfection - Systematic cycle-based positioning captures most gains without needing to pick exact tops
This cycle validated the pattern despite unique factors - ETFs, pre-halving ATH, and institutional adoption didn’t break the fundamental cycle dynamics
2026 likely brings bear market - Historical patterns suggest 12-24 months of decline/consolidation before 2028 halving cycle
Market is maturing - Lower volatility, stronger support levels, better infrastructure
Conclusion
As we close out 2025, the fourth Bitcoin halving cycle has provided another validation of the remarkable four-year pattern. The cycle peaked in October 2025 at $126,210 - almost exactly 18 months after the April 2024 halving, right in line with historical precedent.
For systematic traders, the lesson is clear: cycle-aware positioning - being heavily allocated during months 6-18 post-halving, then defensive in months 18+ - continues to be a robust framework.
As we enter 2026, we’re likely transitioning into the bear market phase of this cycle. Historical patterns suggest:
12-24 months of distribution and decline
Potential bottom in $45,000-$70,000 range
Accumulation opportunity in 2026-2027
Next halving cycle begins April 2028
The traders who profit most are those who accumulate during bear markets (like the coming 2026-2027), hold through early cycles, and distribute during euphoria. We’re currently in the distribution phase - time to be defensive and prepare for the next accumulation opportunity.
Happy Trading!
Alex
Disclaimer: This analysis is for educational purposes only and should not be considered financial advice. Cryptocurrency investments carry significant risk. Always conduct your own research and consult with qualified financial advisors before making investment decisions.




