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 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")