Switch to simpler log-based projection.

This commit is contained in:
sam
2024-11-14 22:29:34 -08:00
parent 3e96a320b9
commit 588c4a8fa4
+37 -43
View File
@@ -4,6 +4,7 @@ from datetime import datetime, timedelta
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import seaborn as sns import seaborn as sns
from scipy.stats import norm from scipy.stats import norm
from scipy.signal import savgol_filter
# Utility functions # Utility functions
@@ -102,50 +103,49 @@ def get_nice_price_points(min_price, max_price):
# Analysis functions # Analysis functions
def analyze_cycles_with_halvings(df): def analyze_trends(df):
"""Analyze Bitcoin market cycles aligned with halving events""" """
Analyze Bitcoin price trends using log returns.
"""
df = df.copy() df = df.copy()
# Get halving dates # Get halving dates and calculate cycle position
halving_dates = get_halving_dates() halving_dates = get_halving_dates()
# Calculate cycle position for each date
df["Cycle_Position"] = df["Date"].apply( df["Cycle_Position"] = df["Date"].apply(
lambda x: get_cycle_position(x, halving_dates) lambda x: get_cycle_position(x, halving_dates)
) )
# Convert to days within cycle (0 to ~1460 days)
df["Cycle_Days"] = (df["Cycle_Position"] * 4 * 365).round().astype(int) df["Cycle_Days"] = (df["Cycle_Position"] * 4 * 365).round().astype(int)
# Calculate returns at different scales # Calculate log returns
df["Returns_30d"] = df["Close"].pct_change(periods=30) df["Log_Price"] = np.log(df["Close"])
df["Returns_90d"] = df["Close"].pct_change(periods=90) df["Log_Return"] = df["Log_Price"].diff()
df["Returns_365d"] = df["Close"].pct_change(periods=365)
# Group by position in cycle and calculate average returns # Group by position in cycle
cycle_returns = df.groupby(df["Cycle_Days"])["Daily_Return"].mean() position_returns = df.groupby("Cycle_Days")["Log_Return"].mean()
cycle_volatility = df.groupby(df["Cycle_Days"])["Daily_Return"].std()
# Smooth the cycle returns to reduce noise
from scipy.signal import savgol_filter
# Smooth the returns using Savitzky-Golay filter
window = 91 # About 3 months window = 91 # About 3 months
if len(cycle_returns) > window: if len(position_returns) > window:
cycle_returns = pd.Series( position_returns = pd.Series(
savgol_filter(cycle_returns, window, 3), index=cycle_returns.index savgol_filter(position_returns, window, 3), index=position_returns.index
) )
return cycle_returns, cycle_volatility return position_returns
def project_prices_with_cycles( def project_prices(
df, days_forward=365, simulations=1000, confidence_levels=[0.95, 0.68] df, days_forward=365, simulations=1000, confidence_levels=[0.95, 0.68]
): ):
""" """
Project future Bitcoin prices using Monte Carlo simulation with halving-aligned cycles. Project future Bitcoin prices using Monte Carlo simulation.
""" """
# Analyze historical cycles # Calculate log returns for volatility estimation
cycle_returns, cycle_volatility = analyze_cycles_with_halvings(df) df = df.copy()
df["Log_Price"] = np.log(df["Close"])
df["Log_Return"] = df["Log_Price"].diff()
# Get cycle-based trends
cycle_trends = analyze_trends(df)
# Get current position in halving cycle # Get current position in halving cycle
halving_dates = get_halving_dates() halving_dates = get_halving_dates()
@@ -153,7 +153,7 @@ def project_prices_with_cycles(
cycle_position = get_cycle_position(current_date, halving_dates) cycle_position = get_cycle_position(current_date, halving_dates)
current_cycle_days = int(cycle_position * 4 * 365) current_cycle_days = int(cycle_position * 4 * 365)
# Current price (last known price) # Current price and date
last_price = df["Close"].iloc[-1] last_price = df["Close"].iloc[-1]
last_date = df["Date"].iloc[-1] last_date = df["Date"].iloc[-1]
@@ -167,15 +167,11 @@ def project_prices_with_cycles(
(current_cycle_days + i) % (4 * 365) for i in range(days_forward) (current_cycle_days + i) % (4 * 365) for i in range(days_forward)
] ]
expected_returns = np.array( expected_returns = np.array(
[cycle_returns.get(day, cycle_returns.mean()) for day in future_cycle_days] [cycle_trends.get(day, cycle_trends.mean()) for day in future_cycle_days]
) )
# Calculate base volatility (recent) # Calculate volatility using recent data
recent_volatility = df["Daily_Return"].tail(90).std() recent_volatility = df["Log_Return"].tail(90).std()
# Add long-term trend component (very gentle decay)
long_term_decay = 0.9 ** (np.arange(days_forward) / 365) # 10% reduction per year
expected_returns = expected_returns * long_term_decay
# Run Monte Carlo simulation # Run Monte Carlo simulation
np.random.seed(42) # For reproducibility np.random.seed(42) # For reproducibility
@@ -186,9 +182,9 @@ def project_prices_with_cycles(
returns = np.random.normal( returns = np.random.normal(
loc=expected_returns, scale=recent_volatility, size=days_forward loc=expected_returns, scale=recent_volatility, size=days_forward
) )
# Since we're using log returns, we can simply sum them
# Calculate price path cumulative_returns = np.cumsum(returns)
price_path = last_price * np.exp(np.cumsum(returns)) price_path = last_price * np.exp(cumulative_returns)
simulated_paths[:, sim] = price_path simulated_paths[:, sim] = price_path
# Calculate percentiles for confidence intervals # Calculate percentiles for confidence intervals
@@ -206,7 +202,7 @@ def project_prices_with_cycles(
simulated_paths, upper_percentile, axis=1 simulated_paths, upper_percentile, axis=1
) )
# Add expected trend line (without randomness) # Add expected trend line
results["Expected_Trend"] = last_price * np.exp(np.cumsum(expected_returns)) results["Expected_Trend"] = last_price * np.exp(np.cumsum(expected_returns))
return results return results
@@ -301,11 +297,11 @@ def create_plots(df, start=None, end=None, project_days=365):
raise ValueError("No data found for the specified date range") raise ValueError("No data found for the specified date range")
# Generate projections # Generate projections
cycle_returns, cycle_volatility = analyze_cycles_with_halvings(plot_df) # cycle_returns, cycle_volatility = analyze_trends(plot_df)
projections = project_prices_with_cycles(plot_df, days_forward=project_days) projections = project_prices(plot_df, days_forward=project_days)
# Create cycle visualization # Create cycle visualization
visualize_cycle_patterns(plot_df, cycle_returns, cycle_volatility) # visualize_cycle_patterns(plot_df, cycle_returns, cycle_volatility)
# Set up the style # Set up the style
plt.style.use("seaborn-v0_8") plt.style.use("seaborn-v0_8")
@@ -743,9 +739,7 @@ def create_backtest_plot(
raise ValueError("Insufficient training data before backtest date") raise ValueError("Insufficient training data before backtest date")
# Generate historical projections using only training data # Generate historical projections using only training data
historical_projections = project_prices_with_cycles( historical_projections = project_prices(training_df, days_forward=project_days)
training_df, days_forward=project_days
)
# Set up the plot # Set up the plot
plt.style.use("seaborn-v0_8") plt.style.use("seaborn-v0_8")