From f023f7e99989ba7626e84f2238322d49859d7611 Mon Sep 17 00:00:00 2001 From: Sam Fredrickson Date: Sat, 16 Nov 2024 20:23:59 -0800 Subject: [PATCH] Improve uncertainty estimation. --- model.py | 146 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 115 insertions(+), 31 deletions(-) diff --git a/model.py b/model.py index 5b97722..96bca4d 100644 --- a/model.py +++ b/model.py @@ -222,44 +222,136 @@ def adjust_trend_expectations(expected_returns, cycle_position): return expected_returns * damping_factor -def get_projection_adjustments(days_forward, current_cycle_position): +def calculate_market_conditions(df, lookback_window=180): """ - Final projection adjustments with precise uncertainty scaling. + Calculate market condition metrics to inform uncertainty scaling. + """ + df = df.copy() # Avoid modifying original dataframe + metrics = {} + + # Use log returns for stability + df["Log_Return"] = np.log(df["Close"]).diff() + + # Handle initial NaN values + df["Log_Return"] = df["Log_Return"].fillna(method="bfill") + + # Recent vs historical volatility ratio + recent_vol = max(df["Log_Return"].tail(30).std(), 1e-8) # Prevent division by zero + historical_vol = max(df["Log_Return"].tail(lookback_window).std(), 1e-8) + metrics["vol_ratio"] = recent_vol / historical_vol + + # Trend strength using log prices + log_prices = np.log(df["Close"]) + ma50 = log_prices.rolling(50, min_periods=1).mean() + ma200 = log_prices.rolling(200, min_periods=1).mean() + metrics["trend_strength"] = (ma50.iloc[-1] - ma200.iloc[-1]) / historical_vol + + # Drawdown intensity + rolling_max = df["Close"].rolling(lookback_window, min_periods=1).max() + current_drawdown = df["Close"].iloc[-1] / rolling_max.iloc[-1] - 1 + metrics["drawdown"] = abs(min(current_drawdown, 0)) + + return metrics + + +def get_projection_adjustments(days_forward, current_cycle_position, df): + """ + Enhanced projection adjustments with dynamic uncertainty scaling. """ adjustments = np.ones(days_forward) - # Fixed base uncertainty with slight cycle variation + # Get market condition metrics + conditions = calculate_market_conditions(df) + + # Base uncertainty varies with market conditions base_uncertainty = 0.016 # Standard rate + + # Increase uncertainty if volatility is unusually high or low + vol_factor = 1 + 0.2 * abs(1 - conditions["vol_ratio"]) + + # Increase uncertainty during strong trends (both up and down) + trend_factor = 1 + 0.15 * abs(conditions["trend_strength"]) + + # Increase uncertainty during significant drawdowns + drawdown_factor = 1 + 0.25 * conditions["drawdown"] + + # Combine factors with cycle position if current_cycle_position > 0.75: - base_uncertainty *= 1.1 # 10% increase late cycle + cycle_factor = 1.15 # Higher uncertainty late in cycle + else: + cycle_factor = 1.0 + # Calculate time-varying uncertainty for i in range(days_forward): - # Conservative growth with fixed cap - time_factor = min(1 + (i / 365) * base_uncertainty, 1.055) # Lower cap + # Conservative growth with cycle and condition awareness + time_factor = min( + 1 + + (i / 365) + * base_uncertainty + * vol_factor + * trend_factor + * drawdown_factor, + 1.20, + ) - # Simpler cycle factors + # Update cycle position for this future point cycle_position = (current_cycle_position + i / 1460) % 1 if cycle_position > 0.75: - cycle_factor = 0.94 + local_cycle_factor = 1.15 else: - cycle_factor = 0.96 + local_cycle_factor = 1.0 - adjustments[i] = time_factor * cycle_factor + # Apply all factors including the initial cycle factor + adjustments[i] = time_factor * local_cycle_factor * cycle_factor + + # Add minimum floor to prevent overconfidence + adjustments[i] = max(adjustments[i], 1.02 + (i / 365) * 0.01) return adjustments +def calculate_confidence_intervals(simulated_paths, confidence_levels=[0.95, 0.68]): + """ + Calculate confidence intervals with dynamic quantile selection based on market conditions. + """ + results = {} + + for level in confidence_levels: + # Calculate standard error of the median + median_std = np.std( + [np.median(simulated_paths[:, i]) for i in range(simulated_paths.shape[1])] + ) + + # Adjust quantiles based on estimation uncertainty + adjustment = min(0.1, median_std / np.median(simulated_paths)) # Cap adjustment + + # Widen intervals slightly when uncertainty is high + effective_level = level + (1 - level) * adjustment + + lower_percentile = (1 - effective_level) * 100 / 2 + upper_percentile = 100 - lower_percentile + + 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 + ) + + return results + + def project_prices( df, days_forward=365, simulations=1000, confidence_levels=[0.95, 0.68] ): """ - Project future Bitcoin prices with simplified calibration. + Modified projection function incorporating enhanced uncertainty estimation. """ df = df.copy() df["Log_Price"] = np.log(df["Close"]) df["Log_Return"] = df["Log_Price"].diff() - # Get current cycle position + # Get halving dates and current cycle position halving_dates = get_halving_dates() current_date = df["Date"].max() cycle_position = get_cycle_position(current_date, halving_dates) @@ -305,11 +397,13 @@ def project_prices( era_params = era_adjustments[current_era] - # Get projection adjustments for scaling uncertainty over time - projection_adjustments = get_projection_adjustments(days_forward, cycle_position) + # Get projection adjustments with market awareness + projection_adjustments = get_projection_adjustments( + days_forward, cycle_position, df + ) # Run Monte Carlo simulation - np.random.seed(42) + np.random.seed(42) # Restored for reproducibility simulated_paths = np.zeros((days_forward, simulations)) for sim in range(simulations): @@ -328,25 +422,15 @@ def project_prices( price_path = last_price * np.exp(cumulative_returns) simulated_paths[:, sim] = price_path - # Calculate results + # Calculate results with dynamic confidence intervals results = pd.DataFrame(index=future_dates) results["Median"] = np.percentile(simulated_paths, 50, axis=1) + results["Expected_Trend"] = last_price * np.exp(np.cumsum(drift)) - # Calculate Expected_Trend using adjusted drift - cumulative_drift = np.cumsum(drift) - results["Expected_Trend"] = last_price * np.exp(cumulative_drift) - - # Calculate confidence intervals - for level in confidence_levels: - lower_percentile = (1 - level) * 100 / 2 - upper_percentile = 100 - lower_percentile - - 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 - ) + # Calculate confidence intervals with dynamic adjustment + ci_results = calculate_confidence_intervals(simulated_paths, confidence_levels) + for key, values in ci_results.items(): + results[key] = values return results