From 7223144b139abda10127df8c557d11fd2bc31681 Mon Sep 17 00:00:00 2001 From: Sam Fredrickson Date: Tue, 19 Nov 2024 08:45:40 -0800 Subject: [PATCH] Use S2F metrics for trend analysis. --- model.py | 183 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 112 insertions(+), 71 deletions(-) diff --git a/model.py b/model.py index b0756d4..710be43 100644 --- a/model.py +++ b/model.py @@ -280,13 +280,30 @@ class MarketFundamentals: return np.clip(adjustment, 0.65, 0.75) def calculate_confidence_adjustment(self, metrics, level): - """Calculate how much to adjust confidence intervals based on market conditions.""" - depth_impact = np.clip(metrics["market_depth"] * 0.2, 0, 0.2) - vol_impact = np.clip(metrics["volume_to_supply"] * 30, 0, 0.2) - total_adjustment = (depth_impact + vol_impact) * 0.5 + """Calculate confidence interval adjustments with more sensitive market metrics.""" + # More granular depth impact + if metrics["market_depth"] > 0.1: + depth_factor = 0.5 + elif metrics["market_depth"] > 0.05: + depth_factor = 0.7 + else: + depth_factor = 1.0 + + # More granular volume impact + if metrics["volume_to_supply"] > 0.015: + volume_factor = 0.5 + elif metrics["volume_to_supply"] > 0.008: + volume_factor = 0.7 + else: + volume_factor = 1.0 + + depth_impact = np.clip(metrics["market_depth"] * 0.15 * depth_factor, 0, 0.15) + vol_impact = np.clip(metrics["volume_to_supply"] * 20 * volume_factor, 0, 0.15) + + total_adjustment = (depth_impact + vol_impact) * 0.4 if level >= 0.95: - total_adjustment *= 0.5 + total_adjustment *= 0.4 return level + (1 - level) * total_adjustment @@ -329,9 +346,10 @@ def compare_adjustments(df, fundamentals): def analyze_trends(df): """ - Analyze Bitcoin price trends using log returns with asymmetric dampening. + Analyze Bitcoin price trends using log returns with S2F awareness. """ df = df.copy() + fundamentals = MarketFundamentals() # Get halving dates and calculate cycle position halving_dates = get_halving_dates() @@ -340,41 +358,63 @@ def analyze_trends(df): ) df["Cycle_Days"] = (df["Cycle_Position"] * 4 * 365).round().astype(int) - # Calculate log returns + # Calculate S2F metrics for each date + supply_metrics = [ + fundamentals.calculate_supply_metrics(date) for date in df["Date"] + ] + df["S2F_Ratio"] = [m["stock_to_flow"] for m in supply_metrics] + df["S2F_Change"] = df["S2F_Ratio"].pct_change() + + # Calculate log returns and basic cycle returns df["Log_Price"] = np.log(df["Close"]) df["Log_Return"] = df["Log_Price"].diff() - # Group by position in cycle + # Calculate cycle-based returns position_returns = df.groupby("Cycle_Days")["Log_Return"].mean() - # Simple moving average smoothing + # Calculate S2F impact on returns + s2f_impact = df.groupby("Cycle_Days")["S2F_Change"].mean() + + # Smooth both components window = 60 - smoothed_returns = position_returns.rolling( + smoothed_cycle_returns = position_returns.rolling( window=window, center=True, min_periods=int(window / 2), ).mean() - # Fill any NaN values at the edges - smoothed_returns = smoothed_returns.fillna(method="bfill").fillna(method="ffill") + smoothed_s2f_impact = s2f_impact.rolling( + window=window, + center=True, + min_periods=int(window / 2), + ).mean() - # Apply asymmetric dampening to extreme values - returns_std = smoothed_returns.std() + # Fill NaN values + smoothed_cycle_returns = smoothed_cycle_returns.fillna(method="bfill").fillna( + method="ffill" + ) + smoothed_s2f_impact = smoothed_s2f_impact.fillna(method="bfill").fillna( + method="ffill" + ) - def asymmetric_dampen(x): - if x > 2 * returns_std: - return x * 0.6 # Stronger dampening for positive extremes - elif x < -2 * returns_std: - return x * 0.7 # Slightly less dampening for negative extremes - return x + # Combine cycle returns with S2F impact + s2f_weight = 0.3 # Adjustable parameter + combined_returns = ( + smoothed_cycle_returns * (1 - s2f_weight) + smoothed_s2f_impact * s2f_weight + ) - smoothed_returns = smoothed_returns.map(asymmetric_dampen) + # Apply dampening using current market metrics + latest_metrics = fundamentals.get_market_maturity_metrics(df, df["Date"].max()) + market_adjustment = fundamentals.calculate_volatility_adjustment(latest_metrics) - # Additional dampening based on absolute magnitude - magnitude_factor = 0.9 # Global dampening factor - smoothed_returns = smoothed_returns * magnitude_factor + def adaptive_dampen(x): + if x > 2 * combined_returns.std(): + return x * (0.6 * market_adjustment) + elif x < -2 * combined_returns.std(): + return x * (0.7 * market_adjustment) + return x * market_adjustment - return smoothed_returns + return combined_returns.map(adaptive_dampen) def calculate_adaptive_volatility( @@ -615,56 +655,44 @@ def calculate_market_conditions(df, lookback_window=180): def get_projection_adjustments(days_forward, current_cycle_position, df): - """ - Enhanced projection adjustments with dynamic uncertainty scaling. - """ + """Enhanced projection adjustments using market fundamentals.""" adjustments = np.ones(days_forward) + fundamentals = MarketFundamentals() - # Get market condition metrics - conditions = calculate_market_conditions(df) + # Pre-calculate metrics + lookback = min(365, len(df)) + historical_metrics = [ + fundamentals.get_market_maturity_metrics(df, date) + for date in df["Date"].tail(lookback) + ] + historical_avg_volume_to_supply = np.mean( + [m["volume_to_supply"] for m in historical_metrics] + ) - # Base uncertainty varies with market conditions - base_uncertainty = 0.016 # Standard rate + current_metrics = fundamentals.get_market_maturity_metrics(df, df["Date"].max()) + base_uncertainty = 0.016 * (1 + current_metrics["supply_growth_rate"] * 365) + vol_factor = 1 + 0.2 * abs(1 - current_metrics["volume_to_supply"] * 50) + market_depth_factor = 1 / np.sqrt(1 + current_metrics["market_depth"] * 5) + s2f_factor = 1 / np.log1p(current_metrics["stock_to_flow"]) + regime_scale = np.clip( + current_metrics["volume_to_supply"] / historical_avg_volume_to_supply, 0.88, 1.2 + ) - # 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: - 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 cycle and condition awareness time_factor = min( 1 + (i / 365) * base_uncertainty * vol_factor - * trend_factor - * drawdown_factor, + * market_depth_factor + * s2f_factor, 1.20, ) - # Update cycle position for this future point cycle_position = (current_cycle_position + i / 1460) % 1 - if cycle_position > 0.75: - local_cycle_factor = 1.15 - else: - local_cycle_factor = 1.0 + local_cycle_factor = 1.15 if cycle_position > 0.75 else 1.0 - # 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] = time_factor * local_cycle_factor * regime_scale adjustments[i] = max(adjustments[i], 1.02 + (i / 365) * 0.01) return adjustments @@ -765,21 +793,34 @@ def project_prices( # Calculate confidence intervals for level in confidence_levels: - # Get market metrics for confidence interval adjustment metrics = fundamentals.get_market_maturity_metrics(df, current_date) - # Calculate adjusted confidence level - effective_level = fundamentals.calculate_confidence_adjustment(metrics, level) + # Calculate intervals by projection horizon + lower_bounds = [] + upper_bounds = [] - lower_percentile = (1 - effective_level) * 100 / 2 - upper_percentile = 100 - lower_percentile + for day in range(days_forward): + time_factor = ( + 0.85 if day > 365 else 0.9 if day > 180 else 0.95 if day > 90 else 1.0 + ) - 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 - ) + effective_level = ( + fundamentals.calculate_confidence_adjustment(metrics, level) + * time_factor + ) + + lower_percentile = (1 - effective_level) * 100 / 2 + upper_percentile = 100 - lower_percentile + + lower_bounds.append( + np.percentile(simulated_paths[day, :], lower_percentile) + ) + upper_bounds.append( + np.percentile(simulated_paths[day, :], upper_percentile) + ) + + results[f"Lower_{int(level*100)}"] = lower_bounds + results[f"Upper_{int(level*100)}"] = upper_bounds return results