From 588c4a8fa4b395752d0da5ab706bf16d57707cfc Mon Sep 17 00:00:00 2001 From: Sam Fredrickson Date: Thu, 14 Nov 2024 22:29:34 -0800 Subject: [PATCH] Switch to simpler log-based projection. --- model.py | 80 ++++++++++++++++++++++++++------------------------------ 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/model.py b/model.py index b481104..0b6f338 100644 --- a/model.py +++ b/model.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import matplotlib.pyplot as plt import seaborn as sns from scipy.stats import norm +from scipy.signal import savgol_filter # Utility functions @@ -102,50 +103,49 @@ def get_nice_price_points(min_price, max_price): # Analysis functions -def analyze_cycles_with_halvings(df): - """Analyze Bitcoin market cycles aligned with halving events""" +def analyze_trends(df): + """ + Analyze Bitcoin price trends using log returns. + """ df = df.copy() - # Get halving dates + # Get halving dates and calculate cycle position halving_dates = get_halving_dates() - - # Calculate cycle position for each date df["Cycle_Position"] = df["Date"].apply( 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) - # Calculate returns at different scales - df["Returns_30d"] = df["Close"].pct_change(periods=30) - df["Returns_90d"] = df["Close"].pct_change(periods=90) - df["Returns_365d"] = df["Close"].pct_change(periods=365) + # Calculate log returns + df["Log_Price"] = np.log(df["Close"]) + df["Log_Return"] = df["Log_Price"].diff() - # Group by position in cycle and calculate average returns - cycle_returns = df.groupby(df["Cycle_Days"])["Daily_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 + # Group by position in cycle + position_returns = df.groupby("Cycle_Days")["Log_Return"].mean() + # Smooth the returns using Savitzky-Golay filter window = 91 # About 3 months - if len(cycle_returns) > window: - cycle_returns = pd.Series( - savgol_filter(cycle_returns, window, 3), index=cycle_returns.index + if len(position_returns) > window: + position_returns = pd.Series( + 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] ): """ - Project future Bitcoin prices using Monte Carlo simulation with halving-aligned cycles. + Project future Bitcoin prices using Monte Carlo simulation. """ - # Analyze historical cycles - cycle_returns, cycle_volatility = analyze_cycles_with_halvings(df) + # Calculate log returns for volatility estimation + 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 halving_dates = get_halving_dates() @@ -153,7 +153,7 @@ def project_prices_with_cycles( cycle_position = get_cycle_position(current_date, halving_dates) current_cycle_days = int(cycle_position * 4 * 365) - # Current price (last known price) + # Current price and date last_price = df["Close"].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) ] 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) - recent_volatility = df["Daily_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 + # Calculate volatility using recent data + recent_volatility = df["Log_Return"].tail(90).std() # Run Monte Carlo simulation np.random.seed(42) # For reproducibility @@ -186,9 +182,9 @@ def project_prices_with_cycles( returns = np.random.normal( loc=expected_returns, scale=recent_volatility, size=days_forward ) - - # Calculate price path - price_path = last_price * np.exp(np.cumsum(returns)) + # Since we're using log returns, we can simply sum them + cumulative_returns = np.cumsum(returns) + price_path = last_price * np.exp(cumulative_returns) simulated_paths[:, sim] = price_path # Calculate percentiles for confidence intervals @@ -206,7 +202,7 @@ def project_prices_with_cycles( 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)) 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") # Generate projections - cycle_returns, cycle_volatility = analyze_cycles_with_halvings(plot_df) - projections = project_prices_with_cycles(plot_df, days_forward=project_days) + # cycle_returns, cycle_volatility = analyze_trends(plot_df) + projections = project_prices(plot_df, days_forward=project_days) # 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 plt.style.use("seaborn-v0_8") @@ -743,9 +739,7 @@ def create_backtest_plot( raise ValueError("Insufficient training data before backtest date") # Generate historical projections using only training data - historical_projections = project_prices_with_cycles( - training_df, days_forward=project_days - ) + historical_projections = project_prices(training_df, days_forward=project_days) # Set up the plot plt.style.use("seaborn-v0_8")