Switch to simpler log-based projection.
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user