Files
bitcoin-model/model.py
T

1252 lines
42 KiB
Python
Raw Normal View History

2024-11-14 16:53:12 -08:00
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.stats import norm
2024-11-14 22:29:34 -08:00
from scipy.signal import savgol_filter
2024-11-15 10:22:12 -08:00
from multiprocessing import Process
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Utility functions
def get_halving_dates():
"""Return known and projected Bitcoin halving dates"""
return pd.to_datetime(
[
"2008-01-03", # Bitcoin genesis block (treat as cycle start)
"2012-11-28", # First halving
"2016-07-09", # Second halving
"2020-05-11", # Third halving
"2024-04-19", # Fourth halving
"2028-04-20", # Fifth halving (projected)
]
)
def get_cycle_position(date, halving_dates):
2024-11-14 16:53:12 -08:00
"""
2024-11-14 20:05:27 -08:00
Calculate position in halving cycle (0 to 1) for a given date.
0 represents a halving event, 1 represents just before the next halving.
2024-11-14 16:53:12 -08:00
"""
2024-11-14 20:05:27 -08:00
# Convert date to datetime if it's not already
date = pd.to_datetime(date)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Find the most recent halving before this date
prev_halving = halving_dates[halving_dates <= date].max()
if pd.isna(prev_halving):
return 0.0 # For dates before first halving
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Find next halving
future_halvings = halving_dates[halving_dates > date]
if len(future_halvings) == 0:
# For dates after last known halving, use same cycle length as last known cycle
last_cycle_length = (halving_dates[-1] - halving_dates[-2]).days
days_since_halving = (date - halving_dates[-1]).days
return min(days_since_halving / last_cycle_length, 1.0)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
next_halving = future_halvings.min()
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Calculate position as fraction between halvings
days_since_halving = (date - prev_halving).days
cycle_length = (next_halving - prev_halving).days
return min(days_since_halving / cycle_length, 1.0)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
def format_price(x, p):
"""Format large numbers in K, M, B format with appropriate precision"""
if abs(x) >= 1e9:
return f"${x/1e9:.1f}B"
if abs(x) >= 1e6:
return f"${x/1e6:.1f}M"
if abs(x) >= 1e3:
return f"${x/1e3:.1f}K"
if abs(x) >= 1:
return f"${x:.0f}"
return f"${x:.2f}" # For values less than $1, show cents
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
def get_nice_price_points(min_price, max_price):
"""
Generate a reasonable set of price points for the y-axis that look clean
and cover the range without cluttering the chart.
"""
2024-11-14 20:33:31 -08:00
# Handle zero or negative prices
min_price = max(min_price, 0.0001) # Set minimum price to $0.0001
2024-11-14 20:05:27 -08:00
log_min = np.floor(np.log10(min_price))
log_max = np.ceil(np.log10(max_price))
price_points = []
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# For very large ranges (spanning more than 4 orders of magnitude),
# only use powers of 10 and mid-points
if log_max - log_min > 4:
for exp in range(int(log_min), int(log_max + 1)):
base = 10**exp
# Add main power of 10
if min_price <= base <= max_price:
price_points.append(base)
# Add mid-point if range is large enough
if min_price <= base * 5 <= max_price and exp > log_min:
price_points.append(base * 5)
else:
# For smaller ranges, use 1, 2, 5 sequence
for exp in range(int(log_min), int(log_max + 1)):
for mult in [1, 2, 5]:
point = mult * 10**exp
if min_price <= point <= max_price:
price_points.append(point)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
return np.array(price_points)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Analysis functions
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
2024-11-14 22:29:34 -08:00
def analyze_trends(df):
"""
2024-11-15 01:45:27 -08:00
Analyze Bitcoin price trends using log returns with simple moving average smoothing.
2024-11-14 22:29:34 -08:00
"""
2024-11-14 20:05:27 -08:00
df = df.copy()
2024-11-14 22:29:34 -08:00
# Get halving dates and calculate cycle position
2024-11-14 20:05:27 -08:00
halving_dates = get_halving_dates()
df["Cycle_Position"] = df["Date"].apply(
lambda x: get_cycle_position(x, halving_dates)
)
df["Cycle_Days"] = (df["Cycle_Position"] * 4 * 365).round().astype(int)
2024-11-14 22:29:34 -08:00
# Calculate log returns
df["Log_Price"] = np.log(df["Close"])
df["Log_Return"] = df["Log_Price"].diff()
2024-11-14 20:05:27 -08:00
2024-11-14 22:29:34 -08:00
# Group by position in cycle
position_returns = df.groupby("Cycle_Days")["Log_Return"].mean()
2024-11-14 20:05:27 -08:00
2024-11-15 01:45:27 -08:00
# Simple moving average smoothing
window = 60
smoothed_returns = position_returns.rolling(
window=window,
center=True, # Center the window for better trend capture
min_periods=int(window / 2), # Allow partial windows to reduce edge effects
).mean()
# Fill any NaN values at the edges
smoothed_returns = smoothed_returns.fillna(method="bfill").fillna(method="ffill")
2024-11-14 20:05:27 -08:00
2024-11-15 01:45:27 -08:00
return smoothed_returns
2024-11-14 20:05:27 -08:00
2024-11-14 22:46:26 -08:00
def calculate_volatility(df, short_window=30, medium_window=90, long_window=180):
"""
Calculate volatility using multiple timeframes and exponential weighting.
Returns a more nuanced estimate of current market volatility.
"""
df = df.copy()
# Calculate log returns if not already present
if "Log_Return" not in df.columns:
df["Log_Price"] = np.log(df["Close"])
df["Log_Return"] = df["Log_Price"].diff()
# Calculate exponentially weighted volatilities for different timeframes
short_vol = df["Log_Return"].ewm(span=short_window).std().iloc[-1]
medium_vol = df["Log_Return"].ewm(span=medium_window).std().iloc[-1]
long_vol = df["Log_Return"].ewm(span=long_window).std().iloc[-1]
# Blend the estimates with more weight on recent data
base_vol = 0.5 * short_vol + 0.3 * medium_vol + 0.2 * long_vol
# Scale up volatility to target ~68% coverage
volatility_scale = 1.2
return base_vol * volatility_scale
2024-11-15 13:37:14 -08:00
def calculate_market_maturity_score(df):
"""
Calculate a market maturity score (0-1) based on multiple indicators.
Higher scores indicate a more mature market.
"""
df = df.copy()
# 1. Enhanced volume-based metrics
# Use rolling median instead of mean to reduce impact of outliers
df["volume_ma90"] = df["Volume"].rolling(window=90).median()
df["volume_ma365"] = df["Volume"].rolling(window=365).median()
# Calculate relative volume growth using log differences
# This better handles exponential growth in volume over time
volume_growth_90d = np.log(df["volume_ma90"] / df["volume_ma90"].shift(90)).fillna(
0
)
volume_growth_365d = np.log(
df["volume_ma365"] / df["volume_ma365"].shift(365)
).fillna(0)
# Normalize volume growth to rolling volatility of volume
# This adapts to different market epochs
volume_growth_std_90 = volume_growth_90d.rolling(window=90).std()
volume_growth_std_365 = volume_growth_365d.rolling(window=365).std()
normalized_volume_growth = (
2024-11-15 15:12:36 -08:00
(volume_growth_90d / volume_growth_std_90).clip(-2, 2) * 0.4
+ (volume_growth_365d / volume_growth_std_365).clip(-2, 2) * 0.6
).fillna(0)
# Transform to 0-1 scale using sigmoid function
volume_score = 1 / (1 + np.exp(-normalized_volume_growth))
2024-11-15 13:37:14 -08:00
# 2. Volatility maturity (lower volatility = more mature)
df["rolling_vol_90"] = df["Daily_Return"].rolling(window=90).std() * np.sqrt(365)
df["rolling_vol_365"] = df["Daily_Return"].rolling(window=365).std() * np.sqrt(365)
# Normalize volatility relative to its historical range
vol_score_90 = 1 / (
1 + df["rolling_vol_90"] / df["rolling_vol_90"].rolling(window=365).median()
)
vol_score_365 = 1 / (
1 + df["rolling_vol_365"] / df["rolling_vol_365"].rolling(window=730).median()
2024-11-15 13:37:14 -08:00
)
vol_maturity = vol_score_90 * 0.4 + vol_score_365 * 0.6
# 3. Market efficiency score using multiple timeframes
efficiency_scores = []
for window in [30, 90]:
# Calculate absolute autocorrelation at multiple lags
for lag in [1, 2, 3, 5]:
autocorr = (
df["Daily_Return"]
.rolling(window=window)
.apply(lambda x: abs(pd.Series(x).autocorr(lag)))
)
efficiency_scores.append(1 - autocorr)
2024-11-15 13:37:14 -08:00
efficiency = pd.concat(efficiency_scores, axis=1).mean(axis=1)
2024-11-15 13:37:14 -08:00
# 4. Futures market impact (post-2017)
futures_date = pd.Timestamp("2017-12-10")
futures_impact = (df["Date"] > futures_date).astype(float)
# Progressive futures market maturation
days_since_futures = (df["Date"] - futures_date).dt.total_seconds() / (24 * 60 * 60)
futures_maturity = futures_impact * (1 - np.exp(-days_since_futures / 365))
# Combine scores with dynamic weights
base_weights = {
"volume": 0.25,
"volatility": 0.30,
"efficiency": 0.25,
"futures": 0.20,
}
# Adjust weights based on data availability
lookback = pd.Timestamp("2016-01-01")
historical_period = (df["Date"] < lookback).astype(float)
2024-11-15 15:12:36 -08:00
# Add stronger early-market adjustment
if df["Date"].min() < pd.Timestamp("2013-01-01"):
historical_period *= 1.5 # Increase uncertainty for pre-2013 data
# Reduce weight of futures impact for historical data
weights = base_weights.copy()
weights["futures"] = weights["futures"] * (1 - historical_period)
# Redistribute futures weight to other components in historical period
historical_adjustment = (weights["futures"] * historical_period) / 3
weights["volume"] += historical_adjustment
weights["volatility"] += historical_adjustment
weights["efficiency"] += historical_adjustment
# Calculate final score
2024-11-15 13:37:14 -08:00
maturity_score = (
weights["volume"] * volume_score
2024-11-15 13:37:14 -08:00
+ weights["volatility"] * vol_maturity
+ weights["efficiency"] * efficiency
+ weights["futures"] * futures_maturity
2024-11-15 13:37:14 -08:00
)
# Apply non-linear transformation to better distinguish maturity levels
maturity_score = 1 / (1 + np.exp(-4 * (maturity_score - 0.5)))
# Final smoothing
maturity_score = maturity_score.rolling(
window=30, min_periods=1, center=True
).mean()
2024-11-15 13:37:14 -08:00
return maturity_score
def adjust_projections_for_maturity(df, projections, maturity_score):
"""
Adjust price projections based on market maturity score.
More mature markets should have tighter confidence intervals
and more conservative growth expectations.
"""
# Get final maturity score
final_maturity = maturity_score.iloc[-1]
# Adjust confidence intervals based on maturity
# More mature markets = tighter intervals
ci_adjustment = 1 - (final_maturity * 0.3) # Max 30% reduction in interval width
2024-11-15 15:12:36 -08:00
# Reduce conservatism in high-maturity periods
if final_maturity > 0.7:
ci_adjustment *= 1.2 # Widen intervals less in very mature periods
2024-11-15 13:37:14 -08:00
# Adjust expected returns based on maturity
# More mature markets = more conservative growth
returns_adjustment = 1 - (
final_maturity * 0.2
) # Max 20% reduction in expected returns
adjusted_projections = projections.copy()
# Adjust confidence intervals
for ci in [68, 95]:
upper_key = f"Upper_{ci}"
lower_key = f"Lower_{ci}"
median = adjusted_projections["Median"]
# Calculate distances from median
upper_distance = adjusted_projections[upper_key] - median
lower_distance = median - adjusted_projections[lower_key]
# Apply maturity-based adjustment
adjusted_projections[upper_key] = median + (upper_distance * ci_adjustment)
adjusted_projections[lower_key] = median - (lower_distance * ci_adjustment)
# Adjust expected trend
trend_distance = (
adjusted_projections["Expected_Trend"] - adjusted_projections["Median"]
)
adjusted_projections["Expected_Trend"] = (
adjusted_projections["Median"] + trend_distance * returns_adjustment
)
return adjusted_projections
2024-11-14 22:29:34 -08:00
def project_prices(
2024-11-14 20:05:27 -08:00
df, days_forward=365, simulations=1000, confidence_levels=[0.95, 0.68]
):
2024-11-14 16:53:12 -08:00
"""
2024-11-15 15:12:36 -08:00
Project future Bitcoin prices using Monte Carlo simulation with era-specific adjustments.
2024-11-14 16:53:12 -08:00
"""
2024-11-15 13:37:14 -08:00
# Calculate market maturity score
maturity_score = calculate_market_maturity_score(df)
2024-11-15 15:12:36 -08:00
final_maturity = maturity_score.iloc[-1]
2024-11-15 13:37:14 -08:00
2024-11-14 22:29:34 -08:00
df = df.copy()
df["Log_Price"] = np.log(df["Close"])
df["Log_Return"] = df["Log_Price"].diff()
2024-11-15 15:12:36 -08:00
# Define market eras and their characteristics
min_date = df["Date"].min()
max_date = df["Date"].max()
era_adjustments = {
"early": {
"start_date": pd.Timestamp("2013-01-01"),
"end_date": pd.Timestamp("2017-12-10"), # Futures introduction
"volatility_scale": 1.5, # Balanced for post-2013 early market
"trend_scale": 0.85, # Moderately conservative trends
"skew_scale": 1.2, # Moderate trend following
},
"transition": {
"start_date": pd.Timestamp("2017-12-10"),
"end_date": pd.Timestamp("2020-01-01"),
"volatility_scale": 1.2, # Slightly elevated uncertainty
"trend_scale": 0.95, # Near-normal trends
"skew_scale": 1.05, # Light trend following
},
"mature": {
"start_date": pd.Timestamp("2020-01-01"),
"end_date": pd.Timestamp("2100-01-01"),
"volatility_scale": 0.9, # Slightly reduced volatility for mature market
"trend_scale": 1.0, # Base case
"skew_scale": 0.95, # Slight reduction in trend following
},
}
# Determine which era we're in
current_era = None
for era, params in era_adjustments.items():
if min_date >= params["start_date"] and min_date < params["end_date"]:
current_era = era
break
if current_era is None:
current_era = "mature" # Default to mature era if no match
2024-11-14 16:53:12 -08:00
2024-11-15 15:12:36 -08:00
# Get era-specific adjustment factors
vol_scale = era_adjustments[current_era]["volatility_scale"]
trend_scale = era_adjustments[current_era]["trend_scale"]
skew_scale = era_adjustments[current_era]["skew_scale"]
# Get cycle trends and position
cycle_trends = analyze_trends(df)
2024-11-14 20:05:27 -08:00
halving_dates = get_halving_dates()
current_date = df["Date"].max()
cycle_position = get_cycle_position(current_date, halving_dates)
current_cycle_days = int(cycle_position * 4 * 365)
2024-11-14 16:53:12 -08:00
2024-11-14 22:29:34 -08:00
# Current price and date
2024-11-14 20:05:27 -08:00
last_price = df["Close"].iloc[-1]
last_date = df["Date"].iloc[-1]
2024-11-14 16:53:12 -08:00
2024-11-15 15:12:36 -08:00
# Generate projection dates
2024-11-14 20:05:27 -08:00
future_dates = pd.date_range(
start=last_date + timedelta(days=1), periods=days_forward, freq="D"
)
2024-11-14 16:53:12 -08:00
2024-11-15 15:12:36 -08:00
# Calculate expected returns
2024-11-14 20:05:27 -08:00
future_cycle_days = [
(current_cycle_days + i) % (4 * 365) for i in range(days_forward)
]
2024-11-15 01:45:27 -08:00
expected_returns = []
for day in future_cycle_days:
base_trend = cycle_trends.get(day, cycle_trends.mean())
# Add slight mean reversion for extreme values
if abs(base_trend) > 2 * cycle_trends.std():
base_trend *= 0.8 # Dampen extreme trends
expected_returns.append(base_trend)
expected_returns = np.array(expected_returns)
2024-11-14 16:53:12 -08:00
2024-11-15 15:12:36 -08:00
# Calculate and adjust volatility
2024-11-15 13:37:14 -08:00
base_volatility = calculate_volatility(df)
2024-11-15 15:12:36 -08:00
volatility = base_volatility * vol_scale
2024-11-15 13:37:14 -08:00
2024-11-15 15:12:36 -08:00
# Adjust expected returns
adjusted_expected_returns = expected_returns * trend_scale
2024-11-14 16:53:12 -08:00
2024-11-15 01:45:27 -08:00
# Run Monte Carlo simulation
2024-11-14 22:46:26 -08:00
np.random.seed(42)
2024-11-14 20:05:27 -08:00
simulated_paths = np.zeros((days_forward, simulations))
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
for sim in range(simulations):
2024-11-15 15:12:36 -08:00
# Calculate skew with era-specific scaling
base_skew = 0.087 * skew_scale
skew = np.sign(adjusted_expected_returns) * base_skew
2024-11-15 13:37:14 -08:00
2024-11-14 20:05:27 -08:00
returns = np.random.normal(
2024-11-15 13:37:14 -08:00
loc=adjusted_expected_returns + skew * volatility,
2024-11-14 22:46:26 -08:00
scale=volatility,
size=days_forward,
2024-11-14 20:05:27 -08:00
)
2024-11-14 22:46:26 -08:00
# Calculate price path
2024-11-14 22:29:34 -08:00
cumulative_returns = np.cumsum(returns)
price_path = last_price * np.exp(cumulative_returns)
2024-11-14 20:05:27 -08:00
simulated_paths[:, sim] = price_path
2024-11-14 16:53:12 -08:00
2024-11-15 15:12:36 -08:00
# Calculate results
2024-11-14 20:05:27 -08:00
results = pd.DataFrame(index=future_dates)
results["Median"] = np.percentile(simulated_paths, 50, axis=1)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
for level in confidence_levels:
lower_percentile = (1 - level) * 100 / 2
upper_percentile = 100 - lower_percentile
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
results[f"Lower_{int(level*100)}"] = np.percentile(
simulated_paths, lower_percentile, axis=1
)
results[f"Upper_{int(level*100)}"] = np.percentile(
simulated_paths, upper_percentile, axis=1
)
2024-11-14 16:53:12 -08:00
2024-11-15 13:37:14 -08:00
results["Expected_Trend"] = last_price * np.exp(
np.cumsum(adjusted_expected_returns)
)
results["Market_Maturity"] = final_maturity
2024-11-15 15:12:36 -08:00
results["Era"] = current_era
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
return results
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
def analyze_bitcoin_prices(csv_path):
"""
Analyze Bitcoin price data to calculate volatility and growth rates.
"""
# Read CSV with proper data types
df = pd.read_csv(csv_path, parse_dates=[0])
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Print first few rows of raw data to inspect
print("\nFirst few rows of raw data:")
print(df.head())
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Print data info to see types and non-null counts
print("\nDataset Info:")
print(df.info())
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Convert price columns to float and handle any potential formatting issues
2024-11-15 13:37:14 -08:00
numeric_columns = ["Price", "Open", "High", "Low", "Vol."] # Added Volume
for col in numeric_columns:
# Remove any commas and 'K'/'M' suffixes
2024-11-14 20:05:27 -08:00
df[col] = df[col].astype(str).str.replace(",", "")
2024-11-15 13:37:14 -08:00
# Convert K to thousands
df[col] = df[col].str.replace("K", "e3")
# Convert M to millions
df[col] = df[col].str.replace("M", "e6")
# Convert B to billions
df[col] = df[col].str.replace("B", "e9")
# Convert to numeric
2024-11-14 20:05:27 -08:00
df[col] = pd.to_numeric(df[col], errors="coerce")
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Rename columns for clarity
df.columns = ["Date", "Close", "Open", "High", "Low", "Volume", "Change"]
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Sort by date in ascending order
df = df.sort_values("Date")
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Print summary statistics after conversion
print("\nPrice Summary After Conversion:")
2024-11-15 13:37:14 -08:00
print(df[["Close", "Open", "High", "Low", "Volume"]].describe())
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Calculate daily returns
df["Daily_Return"] = df["Close"].pct_change()
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Print first few daily returns to verify calculation
print("\nFirst few daily returns:")
print(df[["Date", "Close", "Daily_Return"]].head())
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Check for any infinite or NaN values
print("\nInfinite or NaN value counts:")
print(df.isna().sum())
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Calculate metrics using 365 days for annualization
analysis = {
"period_start": df["Date"].min().strftime("%Y-%m-%d"),
"period_end": df["Date"].max().strftime("%Y-%m-%d"),
"total_days": len(df),
"daily_volatility": df["Daily_Return"].std(),
"annualized_volatility": df["Daily_Return"].std() * np.sqrt(365),
"total_return": (df["Close"].iloc[-1] / df["Close"].iloc[0] - 1) * 100,
"average_daily_return": df["Daily_Return"].mean() * 100,
"average_annual_return": ((1 + df["Daily_Return"].mean()) ** 365 - 1) * 100,
"min_price": df["Low"].min(),
"max_price": df["High"].max(),
"avg_price": df["Close"].mean(),
"start_price": df["Close"].iloc[0],
"end_price": df["Close"].iloc[-1],
}
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Calculate rolling metrics
df["Rolling_Volatility_30d"] = df["Daily_Return"].rolling(
window=30
).std() * np.sqrt(365)
df["Rolling_Return_30d"] = df["Close"].pct_change(periods=30) * 100
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
return analysis, df
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Main plotting functions
2024-11-14 16:53:12 -08:00
def create_plots(df, start=None, end=None, project_days=365):
"""
2024-11-15 14:08:16 -08:00
Create enhanced plots including market maturity visualization.
2024-11-14 16:53:12 -08:00
"""
# Filter data based on date range
mask = pd.Series(True, index=df.index)
if start:
2024-11-14 20:05:27 -08:00
mask &= df["Date"] >= pd.to_datetime(start)
2024-11-14 16:53:12 -08:00
if end:
2024-11-14 20:05:27 -08:00
mask &= df["Date"] <= pd.to_datetime(end)
2024-11-14 16:53:12 -08:00
plot_df = df[mask].copy()
if len(plot_df) == 0:
raise ValueError("No data found for the specified date range")
2024-11-15 13:37:14 -08:00
# Calculate market maturity score
maturity_score = calculate_market_maturity_score(plot_df)
plot_df["Market_Maturity"] = maturity_score
2024-11-14 16:53:12 -08:00
2024-11-15 14:08:16 -08:00
# Generate projections
2024-11-15 13:37:14 -08:00
projections = project_prices(plot_df, days_forward=project_days)
2024-11-14 16:53:12 -08:00
# Set up the style
2024-11-14 20:05:27 -08:00
plt.style.use("seaborn-v0_8")
2024-11-14 16:53:12 -08:00
2024-11-15 14:08:16 -08:00
# Create figure with adjusted size for additional subplot
fig = plt.figure(figsize=(15, 20)) # Increased height to accommodate new subplot
2024-11-14 16:53:12 -08:00
# Date range for titles
hist_date_range = f" ({plot_df['Date'].min().strftime('%Y-%m-%d')} to {plot_df['Date'].max().strftime('%Y-%m-%d')})"
# 1. Price history and projections (log scale)
2024-11-15 14:08:16 -08:00
ax1 = plt.subplot(5, 1, 1)
2024-11-14 16:53:12 -08:00
# Plot historical prices
2024-11-14 20:05:27 -08:00
ax1.semilogy(plot_df["Date"], plot_df["Close"], "b-", label="Historical Price")
2024-11-14 16:53:12 -08:00
# Plot projections
2024-11-14 20:05:27 -08:00
ax1.semilogy(
projections.index,
projections["Expected_Trend"],
"--",
color="purple",
label="Expected Trend",
)
ax1.semilogy(
projections.index,
projections["Median"],
":",
color="green",
label="Simulated Median",
)
ax1.fill_between(
projections.index,
projections["Lower_95"],
projections["Upper_95"],
alpha=0.2,
color="orange",
label="95% Confidence Interval",
)
ax1.fill_between(
projections.index,
projections["Lower_68"],
projections["Upper_68"],
alpha=0.3,
color="green",
label="68% Confidence Interval",
)
2024-11-14 16:53:12 -08:00
# Customize y-axis
ax1.yaxis.set_major_formatter(plt.FuncFormatter(format_price))
2024-11-14 20:05:27 -08:00
min_price = min(plot_df["Low"].min(), projections["Lower_95"].min())
max_price = max(plot_df["High"].max(), projections["Upper_95"].max())
2024-11-14 16:53:12 -08:00
price_points = get_nice_price_points(min_price, max_price)
ax1.set_yticks(price_points)
2024-11-15 14:08:16 -08:00
ax1.tick_params(axis="y", labelsize=8)
2024-11-14 16:53:12 -08:00
ax1.margins(y=0.02)
2024-11-14 20:05:27 -08:00
ax1.grid(True, which="major", linestyle="-", alpha=0.5)
ax1.grid(True, which="minor", linestyle=":", alpha=0.2)
ax1.set_title("Bitcoin Price History and Projections (Log Scale)" + hist_date_range)
2024-11-14 16:53:12 -08:00
ax1.legend(fontsize=8)
2024-11-15 14:08:16 -08:00
# 2. Market Maturity Score
ax2 = plt.subplot(5, 1, 2)
2024-11-14 20:05:27 -08:00
ax2.plot(
2024-11-15 14:08:16 -08:00
plot_df["Date"],
plot_df["Market_Maturity"],
color="purple",
label="Market Maturity Score",
)
ax2.set_title("Market Maturity Score" + hist_date_range)
ax2.set_ylim(0, 1)
ax2.grid(True, alpha=0.3)
ax2.legend()
# Add futures launch annotation
futures_date = pd.Timestamp("2017-12-10")
if futures_date >= plot_df["Date"].min() and futures_date <= plot_df["Date"].max():
ax2.axvline(futures_date, color="red", linestyle="--", alpha=0.5)
ax2.text(
futures_date, 0.95, "Futures\nLaunch", rotation=90, va="top", ha="right"
)
# 3. Rolling volatility
ax3 = plt.subplot(5, 1, 3)
ax3.plot(
2024-11-14 20:05:27 -08:00
plot_df["Date"],
plot_df["Rolling_Volatility_30d"],
"r-",
label="30-Day Rolling Volatility",
)
2024-11-15 14:08:16 -08:00
ax3.set_title("30-Day Rolling Volatility (Annualized)" + hist_date_range)
ax3.set_ylabel("Volatility")
ax3.grid(True)
ax3.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: "{:.0%}".format(y)))
ax3.legend()
2024-11-14 16:53:12 -08:00
2024-11-15 14:08:16 -08:00
# 4. Returns distribution
ax4 = plt.subplot(5, 1, 4)
2024-11-14 20:05:27 -08:00
returns_mean = plot_df["Daily_Return"].mean()
returns_std = plot_df["Daily_Return"].std()
filtered_returns = plot_df["Daily_Return"][
(plot_df["Daily_Return"] > returns_mean - 5 * returns_std)
& (plot_df["Daily_Return"] < returns_mean + 5 * returns_std)
2024-11-14 16:53:12 -08:00
]
2024-11-15 14:08:16 -08:00
sns.histplot(filtered_returns, bins=100, ax=ax4)
ax4.set_title(
2024-11-14 20:05:27 -08:00
"Distribution of Daily Returns (Excluding Extreme Outliers)" + hist_date_range
)
2024-11-15 14:08:16 -08:00
ax4.set_xlabel("Daily Return")
ax4.set_ylabel("Count")
ax4.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: "{:.0%}".format(x)))
2024-11-14 16:53:12 -08:00
2024-11-15 14:08:16 -08:00
# Add mean line
ax4.axvline(filtered_returns.mean(), color="r", linestyle="dashed", linewidth=1)
ax4.text(
2024-11-14 20:05:27 -08:00
filtered_returns.mean(),
2024-11-15 14:08:16 -08:00
ax4.get_ylim()[1],
2024-11-14 20:05:27 -08:00
"Mean",
rotation=90,
va="top",
ha="right",
)
2024-11-14 16:53:12 -08:00
2024-11-15 14:08:16 -08:00
# 5. Projection ranges
ax5 = plt.subplot(5, 1, 5)
timepoints = np.array(range(30, project_days, 30))
2024-11-14 16:53:12 -08:00
timepoints = timepoints[timepoints <= project_days]
ranges = []
labels = []
positions = []
for t in timepoints:
2024-11-15 14:08:16 -08:00
idx = t - 1
2024-11-14 20:05:27 -08:00
ranges.extend(
[
projections["Lower_95"].iloc[idx],
projections["Lower_68"].iloc[idx],
projections["Median"].iloc[idx],
projections["Upper_68"].iloc[idx],
projections["Upper_95"].iloc[idx],
]
)
labels.extend(["95% Lower", "68% Lower", "Median", "68% Upper", "95% Upper"])
2024-11-14 16:53:12 -08:00
positions.extend([t] * 5)
2024-11-15 14:08:16 -08:00
ax5.scatter(positions, ranges, alpha=0.6)
2024-11-14 16:53:12 -08:00
for t in timepoints:
idx = positions.index(t)
2024-11-15 14:08:16 -08:00
ax5.plot([t] * 5, ranges[idx : idx + 5], "k-", alpha=0.3)
2024-11-14 16:53:12 -08:00
2024-11-15 14:08:16 -08:00
ax5.set_yscale("log")
2024-11-14 16:53:12 -08:00
min_price = min(ranges)
max_price = max(ranges)
2024-11-15 14:08:16 -08:00
price_points = get_nice_price_points(min_price, max_price)
ax5.set_yticks(price_points)
ax5.yaxis.set_major_formatter(plt.FuncFormatter(format_price))
ax5.set_title("Projected Price Ranges at Future Timepoints")
ax5.set_xlabel("Days Forward")
ax5.set_ylabel("Price (USD)")
ax5.grid(True, alpha=0.3)
ax5.set_xticks(timepoints)
2024-11-15 13:37:14 -08:00
2024-11-14 16:53:12 -08:00
# Adjust layout
2024-11-15 14:08:16 -08:00
plt.tight_layout(h_pad=1.0) # Increased spacing between subplots
2024-11-14 16:53:12 -08:00
# Save the plot
2024-11-14 20:05:27 -08:00
start_str = start if start else plot_df["Date"].min().strftime("%Y-%m-%d")
end_str = end if end else plot_df["Date"].max().strftime("%Y-%m-%d")
filename = f"bitcoin_analysis_{start_str}_to_{end_str}_with_projections.png"
plt.savefig(filename, dpi=300, bbox_inches="tight")
2024-11-14 16:53:12 -08:00
plt.close()
return projections
2024-11-14 20:05:27 -08:00
def visualize_cycle_patterns(df, cycle_returns, cycle_volatility):
2024-11-14 16:53:12 -08:00
"""
2024-11-14 20:05:27 -08:00
Create enhanced visualization of Bitcoin's behavior across halving cycles.
2024-11-14 16:53:12 -08:00
"""
2024-11-14 20:05:27 -08:00
plt.style.use("seaborn-v0_8")
fig = plt.figure(figsize=(15, 15))
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Create a 3x1 subplot grid with different heights
gs = plt.GridSpec(3, 1, height_ratios=[2, 1, 2], hspace=0.3)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Plot 1: Returns across cycle with confidence bands
ax1 = plt.subplot(gs[0])
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Convert days to percentage through cycle
x_points = np.array(cycle_returns.index) / (4 * 365) * 100
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Calculate rolling mean and standard deviation for confidence bands
window = 30 # 30-day window
rolling_mean = pd.Series(cycle_returns.values).rolling(window=window).mean()
rolling_std = pd.Series(cycle_returns.values).rolling(window=window).std()
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Plot confidence bands
ax1.fill_between(
x_points,
(rolling_mean - 2 * rolling_std) * 100,
(rolling_mean + 2 * rolling_std) * 100,
alpha=0.2,
color="blue",
label="95% Confidence",
2024-11-14 16:53:12 -08:00
)
2024-11-14 20:05:27 -08:00
ax1.fill_between(
x_points,
(rolling_mean - rolling_std) * 100,
(rolling_mean + rolling_std) * 100,
alpha=0.3,
color="blue",
label="68% Confidence",
2024-11-14 16:53:12 -08:00
)
2024-11-14 20:05:27 -08:00
# Plot average returns
ax1.plot(
x_points,
cycle_returns.values * 100,
"b-",
label="Average Daily Return",
linewidth=2,
)
ax1.axhline(y=0, color="gray", linestyle="--", alpha=0.5)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Add vertical lines for each year in cycle
for year in range(1, 4):
ax1.axvline(x=year * 25, color="gray", linestyle=":", alpha=0.3)
ax1.text(
year * 25,
ax1.get_ylim()[1],
f"Year {year}",
rotation=90,
va="top",
ha="right",
alpha=0.7,
2024-11-14 16:53:12 -08:00
)
2024-11-14 20:05:27 -08:00
# Highlight halving points
ax1.axvline(x=0, color="red", linestyle="--", alpha=0.5, label="Halving Event")
ax1.axvline(x=100, color="red", linestyle="--", alpha=0.5)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
ax1.set_title("Bitcoin Return Patterns Across Halving Cycle", pad=20)
ax1.set_xlabel("Position in Cycle (%)")
ax1.set_ylabel("Average Daily Return (%)")
ax1.grid(True, alpha=0.3)
ax1.legend(loc="upper right")
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Plot 2: Volatility across cycle
ax2 = plt.subplot(gs[1])
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Calculate rolling volatility confidence bands
vol_mean = pd.Series(cycle_volatility.values).rolling(window=window).mean()
vol_std = pd.Series(cycle_volatility.values).rolling(window=window).std()
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Plot volatility with confidence bands
annualized_factor = np.sqrt(365) * 100
ax2.fill_between(
x_points,
(vol_mean - 2 * vol_std) * annualized_factor,
(vol_mean + 2 * vol_std) * annualized_factor,
alpha=0.2,
color="red",
label="95% Confidence",
2024-11-14 16:53:12 -08:00
)
2024-11-14 20:05:27 -08:00
ax2.plot(
x_points,
cycle_volatility.values * annualized_factor,
"r-",
label="Annualized Volatility",
linewidth=2,
2024-11-14 16:53:12 -08:00
)
2024-11-14 20:05:27 -08:00
# Add year markers
for year in range(1, 4):
ax2.axvline(x=year * 25, color="gray", linestyle=":", alpha=0.3)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
ax2.axvline(x=0, color="red", linestyle="--", alpha=0.5)
ax2.axvline(x=100, color="red", linestyle="--", alpha=0.5)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
ax2.set_xlabel("Position in Cycle (%)")
ax2.set_ylabel("Volatility (%)")
ax2.grid(True, alpha=0.3)
ax2.legend(loc="upper right")
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Plot 3: Average price trajectory within cycles
ax3 = plt.subplot(gs[2])
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Define a color scheme for cycles
cycle_colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd"]
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Calculate average price path for each cycle
halving_dates = get_halving_dates()
cycles = []
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
for i in range(len(halving_dates) - 1):
cycle_start = halving_dates[i]
cycle_end = halving_dates[i + 1]
cycle_data = df[(df["Date"] >= cycle_start) & (df["Date"] < cycle_end)].copy()
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
if len(cycle_data) > 0:
cycle_data["Cycle_Pct"] = (
(cycle_data["Date"] - cycle_start).dt.total_seconds()
/ (cycle_end - cycle_start).total_seconds()
* 100
)
cycle_data["Normalized_Price"] = (
cycle_data["Close"] / cycle_data["Close"].iloc[0]
)
cycles.append(cycle_data)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Plot each historical cycle with distinct colors
for i, cycle in enumerate(cycles):
ax3.semilogy(
cycle["Cycle_Pct"],
cycle["Normalized_Price"],
color=cycle_colors[i],
alpha=0.7,
label=f'Cycle {i+1} ({cycle["Date"].iloc[0].strftime("%Y")}-{cycle["Date"].iloc[-1].strftime("%Y")})',
)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Calculate and plot average cycle
if cycles:
avg_cycle = pd.concat(
[c.set_index("Cycle_Pct")["Normalized_Price"] for c in cycles], axis=1
)
avg_cycle_mean = avg_cycle.mean(axis=1)
avg_cycle_std = avg_cycle.std(axis=1)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
ax3.semilogy(
avg_cycle_mean.index,
avg_cycle_mean.values,
"k-",
linewidth=2,
label="Average Cycle",
)
ax3.fill_between(
avg_cycle_mean.index,
avg_cycle_mean * np.exp(-2 * avg_cycle_std),
avg_cycle_mean * np.exp(2 * avg_cycle_std),
alpha=0.2,
color="gray",
)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Add year markers
for year in range(1, 4):
ax3.axvline(x=year * 25, color="gray", linestyle=":", alpha=0.3)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
ax3.axvline(x=0, color="red", linestyle="--", alpha=0.5)
ax3.axvline(x=100, color="red", linestyle="--", alpha=0.5)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
ax3.set_title("Price Performance Across Cycles (Normalized)", pad=20)
ax3.set_xlabel("Position in Cycle (%)")
ax3.set_ylabel("Price (Relative to Cycle Start)")
ax3.grid(True, alpha=0.3)
ax3.legend(loc="center left", bbox_to_anchor=(1.02, 0.5))
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Add current cycle position marker on all plots
current_position = get_cycle_position(df["Date"].max(), halving_dates) * 100
for ax in [ax1, ax2, ax3]:
ax.axvline(
x=current_position,
color="green",
linestyle="-",
alpha=0.5,
label="Current Position",
2024-11-14 16:53:12 -08:00
)
2024-11-14 20:05:27 -08:00
# Main title for the figure
fig.suptitle("Bitcoin Halving Cycle Analysis", fontsize=16, y=0.95)
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Adjust layout to prevent legend cutoff
plt.tight_layout()
2024-11-14 16:53:12 -08:00
2024-11-14 20:05:27 -08:00
# Save the plot
plt.savefig("bitcoin_cycle_patterns.png", dpi=300, bbox_inches="tight")
plt.close()
2024-11-14 16:53:12 -08:00
2024-11-14 20:33:31 -08:00
def create_backtest_plot(
df, backtest_date="2020-05-11", start_date="2012-11-28", project_days=1650
):
"""
Create a plot comparing actual price history against model projections from a historical date.
Args:
df: DataFrame with historical price data
backtest_date: Date to start the backtest from (default: third halving)
start_date: Date to start considering historical data (default: first halving)
project_days: Number of days to project forward from backtest date
"""
# Convert dates to datetime
backtest_date = pd.to_datetime(backtest_date)
start_date = pd.to_datetime(start_date)
# Validate dates
if start_date >= backtest_date:
raise ValueError("start_date must be earlier than backtest_date")
# Clean the data: remove rows with zero or invalid prices and filter by date
df = df[(df["Close"] > 0) & (df["Date"] >= start_date)].copy()
# Split data into training (before backtest date) and validation (after backtest date)
training_df = df[df["Date"] <= backtest_date].copy()
validation_df = df[df["Date"] > backtest_date].copy()
# Check if we have enough data
if len(training_df) < 30: # Require at least 30 days of training data
raise ValueError("Insufficient training data before backtest date")
# Generate historical projections using only training data
2024-11-14 22:29:34 -08:00
historical_projections = project_prices(training_df, days_forward=project_days)
2024-11-14 20:33:31 -08:00
# Set up the plot
plt.style.use("seaborn-v0_8")
fig, ax = plt.figure(figsize=(15, 10)), plt.gca()
# Plot training data
2024-11-15 15:12:36 -08:00
heading_label = f'Historical Price (Training: {start_date.strftime("%Y-%m-%d")} to {backtest_date.strftime("%Y-%m-%d")})'
2024-11-14 20:33:31 -08:00
ax.semilogy(
training_df["Date"],
training_df["Close"],
"b-",
2024-11-15 15:12:36 -08:00
label=heading_label,
2024-11-14 20:33:31 -08:00
alpha=0.7,
)
# Plot validation data
ax.semilogy(
validation_df["Date"],
validation_df["Close"],
"g-",
label=f'Actual Price (Validation: {backtest_date.strftime("%Y-%m-%d")} onwards)',
linewidth=2,
)
# Plot projections
ax.semilogy(
historical_projections.index,
historical_projections["Expected_Trend"],
"--",
color="purple",
label="Model Projection (Expected)",
)
ax.semilogy(
historical_projections.index,
historical_projections["Median"],
":",
color="orange",
label="Model Projection (Median)",
)
# Add confidence intervals
ax.fill_between(
historical_projections.index,
historical_projections["Lower_95"],
historical_projections["Upper_95"],
alpha=0.2,
color="orange",
label="95% Confidence Interval",
)
ax.fill_between(
historical_projections.index,
historical_projections["Lower_68"],
historical_projections["Upper_68"],
alpha=0.3,
color="green",
label="68% Confidence Interval",
)
# Customize y-axis
ax.yaxis.set_major_formatter(plt.FuncFormatter(format_price))
# Set custom y-axis ticks
min_price = min(
df["Low"].min(),
historical_projections["Lower_95"].min(),
0.0001, # Set minimum price floor
)
max_price = max(df["High"].max(), historical_projections["Upper_95"].max())
price_points = get_nice_price_points(min_price, max_price)
ax.set_yticks(price_points)
# Add halving lines
halving_dates = get_halving_dates()
relevant_halvings = halving_dates[
(halving_dates >= start_date) & (halving_dates <= validation_df["Date"].max())
]
for date in relevant_halvings:
ax.axvline(date, color="red", linestyle="--", alpha=0.3)
ax.text(
date,
ax.get_ylim()[1],
"Halving",
rotation=90,
va="top",
ha="right",
alpha=0.7,
)
# Calculate and add model performance metrics
if len(validation_df) > 0:
# Create a common date range for comparison
actual_prices = validation_df.set_index("Date")["Close"]
common_dates = actual_prices.index.intersection(historical_projections.index)
if len(common_dates) > 0:
actual_aligned = actual_prices[common_dates]
projections_aligned = historical_projections.loc[common_dates]
# Calculate metrics using aligned data
mape = (
np.mean(
np.abs(
(actual_aligned - projections_aligned["Expected_Trend"])
/ actual_aligned
)
)
* 100
)
coverage_95 = (
np.mean(
(actual_aligned >= projections_aligned["Lower_95"])
& (actual_aligned <= projections_aligned["Upper_95"])
)
* 100
)
coverage_68 = (
np.mean(
(actual_aligned >= projections_aligned["Lower_68"])
& (actual_aligned <= projections_aligned["Upper_68"])
)
* 100
)
rmse = np.sqrt(
np.mean((actual_aligned - projections_aligned["Expected_Trend"]) ** 2)
)
max_error = np.max(
np.abs(actual_aligned - projections_aligned["Expected_Trend"])
)
# Add metrics to plot
metrics_text = (
f"Model Performance Metrics:\n"
f"MAPE: {mape:.1f}%\n"
f"RMSE: ${rmse:,.0f}\n"
f"Max Error: ${max_error:,.0f}\n"
f"95% CI Coverage: {coverage_95:.1f}%\n"
f"68% CI Coverage: {coverage_68:.1f}%"
)
2024-11-15 15:12:36 -08:00
with open(
f'bitcoin_backtest_{start_date.strftime("%Y%m%d")}_to_{backtest_date.strftime("%Y%m%d")}.txt',
"w",
) as f:
f.write(f"{heading_label}\n")
f.write(metrics_text)
2024-11-14 20:33:31 -08:00
ax.text(
0.02,
0.98,
metrics_text,
transform=ax.transAxes,
verticalalignment="top",
bbox=dict(facecolor="white", alpha=0.8),
)
# Customize plot
ax.set_title(
f'Bitcoin Price: Model Backtest\nTraining: {start_date.strftime("%Y-%m-%d")} to {backtest_date.strftime("%Y-%m-%d")}'
)
ax.set_xlabel("Date")
ax.set_ylabel("Price (USD)")
ax.grid(True, which="major", linestyle="-", alpha=0.5)
ax.grid(True, which="minor", linestyle=":", alpha=0.2)
ax.legend(loc="center left", bbox_to_anchor=(1.02, 0.5))
# Adjust layout and save
plt.tight_layout()
filename = f'bitcoin_backtest_{start_date.strftime("%Y%m%d")}_to_{backtest_date.strftime("%Y%m%d")}.png'
plt.savefig(filename, dpi=300, bbox_inches="tight")
plt.close()
return historical_projections
def run_projection(df, start):
projections = create_plots(df, start=start, project_days=365 * 4)
#print("\nProjected Prices at Key Points:")
#print(projections.iloc[[29, 89, 179, 364]].round(2)) # 30, 90, 180, 365 days
2024-11-14 20:33:31 -08:00
2024-11-15 10:22:12 -08:00
def run_backtest(params, df):
print(
f"\nRunning backtest from {params['start_date']} to {params['backtest_date']}"
)
backtest_projections = create_backtest_plot(df, **params)
# Print some key projection points vs actual prices
print("\nBacktest Results - Projected vs Actual Prices:")
validation_df = df[df["Date"] > params["backtest_date"]]
actual_prices = validation_df.set_index("Date")["Close"]
for days in [30, 90, 180, 365]:
target_date = pd.to_datetime(params["backtest_date"]) + pd.Timedelta(days=days)
if (
target_date in actual_prices.index
and target_date in backtest_projections.index
):
projected = backtest_projections.loc[target_date]
actual = actual_prices.loc[target_date]
print(f"\n{days} days out ({target_date.strftime('%Y-%m-%d')}):")
print(f"Actual Price: ${actual:,.2f}")
print(f"Projected (Expected): ${projected['Expected_Trend']:,.2f}")
print(
f"Projected Range: ${projected['Lower_95']:,.2f} - ${projected['Upper_95']:,.2f}"
)
if __name__ == "__main__":
analysis, df = analyze_bitcoin_prices("prices.csv")
procs = []
# Create main projection
projection_starts = [
"2013-01-01",
"2014-01-01",
"2015-01-01",
"2016-07-09",
]
for start in projection_starts:
proc = Process(target=run_projection, args=(df, start))
proc.start()
procs.append(proc)
2024-11-15 10:22:12 -08:00
2024-11-15 13:57:36 -08:00
# Create multiple backtests for different periods
2024-11-14 20:33:31 -08:00
# Create multiple backtests for different periods
backtests = [
2024-11-15 13:57:36 -08:00
# Base case: Second until fourth halving
2024-11-14 20:33:31 -08:00
{
2024-11-15 13:57:36 -08:00
"start_date": "2016-07-09",
2024-11-15 10:22:12 -08:00
"backtest_date": "2024-04-19",
"project_days": 1460,
2024-11-14 20:33:31 -08:00
},
2024-11-15 13:57:36 -08:00
# Post-Futures Window with two cycles of training
2024-11-14 20:33:31 -08:00
{
2024-11-15 13:57:36 -08:00
"start_date": "2013-01-01", # Includes pre-futures for cycle learning
2024-11-15 10:22:12 -08:00
"backtest_date": "2020-05-11",
"project_days": 1460,
2024-11-14 20:33:31 -08:00
},
2024-11-15 13:57:36 -08:00
# Cross-Regime Test with two cycles of training
2024-11-14 20:33:31 -08:00
{
2024-11-15 13:57:36 -08:00
"start_date": "2014-01-01",
"backtest_date": "2021-12-31",
"project_days": 1460,
},
# Recent Window focusing on post-2022 behavior
{
"start_date": "2015-01-01", # About two cycles before 2022
"backtest_date": "2022-01-01",
"project_days": 1460,
},
2024-11-14 20:33:31 -08:00
]
# Run all backtests
for params in backtests:
2024-11-15 10:22:12 -08:00
proc = Process(
target=run_backtest,
args=(
params,
df,
),
2024-11-14 20:33:31 -08:00
)
2024-11-15 10:22:12 -08:00
procs.append(proc)
proc.start()
2024-11-15 10:22:12 -08:00
for proc in procs:
proc.join()