Use market fundamentals intsead of empirical era adjustments.
This commit is contained in:
@@ -151,6 +151,168 @@ def get_nice_price_points(min_price, max_price):
|
|||||||
return np.array(price_points)
|
return np.array(price_points)
|
||||||
|
|
||||||
|
|
||||||
|
# Market metrics
|
||||||
|
|
||||||
|
|
||||||
|
class MarketFundamentals:
|
||||||
|
"""
|
||||||
|
Calculate and track fundamental market metrics for Bitcoin.
|
||||||
|
Designed to be extensible for additional metrics.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Constants
|
||||||
|
self.GENESIS_DATE = pd.Timestamp("2009-01-03")
|
||||||
|
self.BLOCKS_PER_DAY = 144
|
||||||
|
self.HALVING_INTERVAL = 210000 # blocks
|
||||||
|
|
||||||
|
# Volatility adjustment parameters (from our tuning)
|
||||||
|
self.VOLUME_SCALE = 70
|
||||||
|
self.DEPTH_SCALE = 7
|
||||||
|
self.BASE_ADJUSTMENT = 0.68
|
||||||
|
|
||||||
|
def calculate_total_supply(self, date):
|
||||||
|
"""Calculate total Bitcoin supply at a given date."""
|
||||||
|
days_since_genesis = (date - self.GENESIS_DATE).days
|
||||||
|
if days_since_genesis < 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_supply = 0
|
||||||
|
remaining_blocks = days_since_genesis * self.BLOCKS_PER_DAY
|
||||||
|
current_reward = 50
|
||||||
|
|
||||||
|
while remaining_blocks > 0 and current_reward >= 0.01:
|
||||||
|
blocks_at_this_reward = min(remaining_blocks, self.HALVING_INTERVAL)
|
||||||
|
total_supply += blocks_at_this_reward * current_reward
|
||||||
|
remaining_blocks -= blocks_at_this_reward
|
||||||
|
current_reward /= 2
|
||||||
|
|
||||||
|
# Adjust for missed blocks and lost coins
|
||||||
|
total_supply *= 0.95 # Account for varying block times
|
||||||
|
total_supply *= 0.93 # Estimate for lost/inaccessible coins
|
||||||
|
|
||||||
|
return total_supply
|
||||||
|
|
||||||
|
def get_block_reward(self, date):
|
||||||
|
"""Get Bitcoin block reward at a given date."""
|
||||||
|
days_since_genesis = (date - self.GENESIS_DATE).days
|
||||||
|
if days_since_genesis < 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
halvings = days_since_genesis // (4 * 365) # Approximate halving periods
|
||||||
|
return 50 / (2**halvings)
|
||||||
|
|
||||||
|
def calculate_supply_metrics(self, date):
|
||||||
|
"""Calculate supply-related metrics."""
|
||||||
|
total_supply = self.calculate_total_supply(date)
|
||||||
|
block_reward = self.get_block_reward(date)
|
||||||
|
daily_new_supply = block_reward * self.BLOCKS_PER_DAY
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total_supply": total_supply,
|
||||||
|
"daily_new_supply": daily_new_supply,
|
||||||
|
"supply_growth_rate": daily_new_supply / total_supply,
|
||||||
|
"stock_to_flow": total_supply / (daily_new_supply * 365), # Annualized
|
||||||
|
}
|
||||||
|
|
||||||
|
def calculate_market_metrics(self, df, date, window=30):
|
||||||
|
"""Calculate market activity metrics."""
|
||||||
|
recent_data = df[df["Date"] <= date].tail(window)
|
||||||
|
|
||||||
|
if len(recent_data) < window:
|
||||||
|
return {"avg_volume": 0, "price_volatility": 0, "price_impact": 0}
|
||||||
|
|
||||||
|
avg_volume = recent_data["Volume"].mean()
|
||||||
|
price_volatility = recent_data["Close"].pct_change().std()
|
||||||
|
price_impact = recent_data["Close"].std() / recent_data["Close"].mean()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"avg_volume": avg_volume,
|
||||||
|
"price_volatility": price_volatility,
|
||||||
|
"price_impact": price_impact,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_market_maturity_metrics(self, df, date, window=30):
|
||||||
|
"""
|
||||||
|
Combine supply and market metrics to assess market maturity.
|
||||||
|
"""
|
||||||
|
supply_metrics = self.calculate_supply_metrics(date)
|
||||||
|
market_metrics = self.calculate_market_metrics(df, date, window)
|
||||||
|
|
||||||
|
# Calculate combined metrics
|
||||||
|
volume_to_supply = market_metrics["avg_volume"] / supply_metrics["total_supply"]
|
||||||
|
market_depth = volume_to_supply / (market_metrics["price_impact"] + 0.001)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"volume_to_supply": volume_to_supply,
|
||||||
|
"supply_growth_rate": supply_metrics["supply_growth_rate"],
|
||||||
|
"market_depth": market_depth,
|
||||||
|
"stock_to_flow": supply_metrics["stock_to_flow"],
|
||||||
|
"price_impact": market_metrics["price_impact"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def calculate_volatility_adjustment(self, metrics):
|
||||||
|
"""
|
||||||
|
Calculate volatility adjustment based on market metrics.
|
||||||
|
"""
|
||||||
|
# Supply-based component
|
||||||
|
supply_based_vol = np.sqrt(metrics["supply_growth_rate"] * 365 * 100)
|
||||||
|
|
||||||
|
# Market maturity component
|
||||||
|
maturity_factor = 1 - np.clip(
|
||||||
|
metrics["volume_to_supply"] * self.VOLUME_SCALE, 0, 0.6
|
||||||
|
)
|
||||||
|
|
||||||
|
# Market depth component
|
||||||
|
depth_factor = np.clip(
|
||||||
|
1 / np.sqrt(1 + metrics["market_depth"] * self.DEPTH_SCALE), 0.7, 1.3
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combine factors
|
||||||
|
adjustment = (
|
||||||
|
self.BASE_ADJUSTMENT
|
||||||
|
* (1 + supply_based_vol)
|
||||||
|
* maturity_factor
|
||||||
|
* depth_factor
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure reasonable bounds
|
||||||
|
return np.clip(adjustment, 0.65, 0.75)
|
||||||
|
|
||||||
|
|
||||||
|
def compare_adjustments(df, fundamentals):
|
||||||
|
"""
|
||||||
|
Compare fundamental-based adjustments with original era-based ones.
|
||||||
|
"""
|
||||||
|
# Sample dates for comparison
|
||||||
|
date_range = pd.date_range(start=df["Date"].min(), end=df["Date"].max(), freq="30D")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for date in date_range:
|
||||||
|
# Calculate era-based adjustment
|
||||||
|
if date < pd.Timestamp("2017-12-10"):
|
||||||
|
era_adj = 0.71 # early era
|
||||||
|
elif date < pd.Timestamp("2020-01-01"):
|
||||||
|
era_adj = 0.69 # transition era
|
||||||
|
else:
|
||||||
|
era_adj = 0.67 # mature era
|
||||||
|
|
||||||
|
# Calculate fundamental-based adjustment
|
||||||
|
metrics = fundamentals.get_market_maturity_metrics(df, date)
|
||||||
|
fund_adj = fundamentals.calculate_volatility_adjustment(metrics)
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"date": date,
|
||||||
|
"era_adjustment": era_adj,
|
||||||
|
"fundamental_adjustment": fund_adj,
|
||||||
|
"metrics": metrics,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return pd.DataFrame(results)
|
||||||
|
|
||||||
|
|
||||||
# Analysis functions
|
# Analysis functions
|
||||||
|
|
||||||
|
|
||||||
@@ -318,70 +480,83 @@ def calculate_adaptive_volatility(
|
|||||||
|
|
||||||
|
|
||||||
def calculate_volatility(df, short_window=30, medium_window=90, long_window=180):
|
def calculate_volatility(df, short_window=30, medium_window=90, long_window=180):
|
||||||
"""
|
"""Calculate volatility using fundamental metrics and adaptive windows."""
|
||||||
Calculate volatility using adaptive windows and era-specific adjustments.
|
df = df.copy()
|
||||||
Returns a single volatility value.
|
df["Log_Return"] = np.log(df["Close"]).diff()
|
||||||
"""
|
|
||||||
if len(df) < 30:
|
if len(df) < 30:
|
||||||
return 0.02 # Return a reasonable default for very short periods
|
return 0.02 # Reasonable default for very short periods
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Calculate adaptive volatility
|
# Initialize fundamentals calculator
|
||||||
base_vol = calculate_adaptive_volatility(
|
fundamentals = MarketFundamentals()
|
||||||
df,
|
current_date = df["Date"].max()
|
||||||
short_window=short_window,
|
|
||||||
medium_window=medium_window,
|
# Get recent data for efficiency
|
||||||
long_window=long_window,
|
lookback = max(long_window * 2, 360)
|
||||||
|
recent_df = df.iloc[-lookback:].copy() if len(df) > lookback else df.copy()
|
||||||
|
|
||||||
|
# Calculate base volatilities
|
||||||
|
short_vol = recent_df["Log_Return"].ewm(span=short_window, adjust=False).std()
|
||||||
|
medium_vol = recent_df["Log_Return"].ewm(span=medium_window, adjust=False).std()
|
||||||
|
long_vol = recent_df["Log_Return"].ewm(span=long_window, adjust=False).std()
|
||||||
|
|
||||||
|
if short_vol.iloc[-1] == 0 or np.isnan(short_vol.iloc[-1]):
|
||||||
|
return df["Log_Return"].std()
|
||||||
|
|
||||||
|
# Calculate volatility regime indicators
|
||||||
|
medium_vol_mean = medium_vol.rolling(min(90, len(recent_df))).mean()
|
||||||
|
long_vol_mean = long_vol.rolling(min(180, len(recent_df))).mean()
|
||||||
|
|
||||||
|
# Compare short-term to both medium and long-term volatility
|
||||||
|
if medium_vol_mean.iloc[-1] == 0:
|
||||||
|
vol_regime = pd.Series([1.0] * len(recent_df))
|
||||||
|
else:
|
||||||
|
medium_regime = short_vol / medium_vol_mean
|
||||||
|
long_regime = short_vol / long_vol_mean
|
||||||
|
vol_regime = pd.concat([medium_regime, long_regime], axis=1).max(axis=1)
|
||||||
|
|
||||||
|
vol_regime = vol_regime.clip(0.5, 2.0)
|
||||||
|
latest_regime = vol_regime.iloc[-1]
|
||||||
|
|
||||||
|
# Get market metrics
|
||||||
|
metrics = fundamentals.get_market_maturity_metrics(df, current_date)
|
||||||
|
|
||||||
|
# Calculate adaptive weights
|
||||||
|
high_vol_weight = (latest_regime - 0.5) / 1.5 # 1.5 = 2.0 - 0.5
|
||||||
|
base_weights = np.array([0.2, 0.5, 0.3])
|
||||||
|
stress_weights = np.array([0.4, 0.4, 0.2])
|
||||||
|
weights = (
|
||||||
|
base_weights * (1 - high_vol_weight) + stress_weights * high_vol_weight
|
||||||
)
|
)
|
||||||
|
|
||||||
if np.isnan(base_vol) or base_vol == 0:
|
# Calculate final volatilities
|
||||||
base_vol = df["Close"].pct_change().std()
|
final_short = recent_df["Log_Return"].iloc[-short_window:].std()
|
||||||
|
final_medium = recent_df["Log_Return"].iloc[-medium_window:].std()
|
||||||
|
final_long = recent_df["Log_Return"].iloc[-long_window:].std()
|
||||||
|
|
||||||
# Era-specific adjustments
|
if np.isnan([final_short, final_medium, final_long]).any() or 0 in [
|
||||||
start_date = df["Date"].min()
|
final_short,
|
||||||
if start_date >= pd.Timestamp("2020-01-01"):
|
final_medium,
|
||||||
base_adjustment = 0.64
|
final_long,
|
||||||
elif start_date >= pd.Timestamp("2016-07-09"):
|
]:
|
||||||
base_adjustment = 0.67
|
return df["Log_Return"].std()
|
||||||
elif start_date >= pd.Timestamp("2015-01-01"):
|
|
||||||
base_adjustment = 0.69
|
|
||||||
else:
|
|
||||||
base_adjustment = 0.70
|
|
||||||
|
|
||||||
return max(
|
# Apply market-based adjustment
|
||||||
base_vol * base_adjustment, 0.01
|
market_adjustment = fundamentals.calculate_volatility_adjustment(metrics)
|
||||||
) # Ensure we never return 0 volatility
|
|
||||||
|
# Calculate final volatility
|
||||||
|
final_vol = (
|
||||||
|
final_short * weights[0]
|
||||||
|
+ final_medium * weights[1]
|
||||||
|
+ final_long * weights[2]
|
||||||
|
) * market_adjustment
|
||||||
|
|
||||||
|
return max(final_vol, df["Log_Return"].std() * 0.5)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in volatility calculation: {e}")
|
print(f"Error in volatility calculation: {e}")
|
||||||
# Fall back to simple volatility with minimum floor
|
return df["Log_Return"].std()
|
||||||
return max(df["Close"].pct_change().std(), 0.01)
|
|
||||||
|
|
||||||
|
|
||||||
# Era definitions
|
|
||||||
era_adjustments = {
|
|
||||||
"early": {
|
|
||||||
"start_date": pd.Timestamp("2013-01-01"),
|
|
||||||
"end_date": pd.Timestamp("2017-12-10"),
|
|
||||||
"volatility_scale": 0.71, # Slight increase
|
|
||||||
"trend_scale": 0.75,
|
|
||||||
"skew_scale": 1.0,
|
|
||||||
},
|
|
||||||
"transition": {
|
|
||||||
"start_date": pd.Timestamp("2017-12-10"),
|
|
||||||
"end_date": pd.Timestamp("2020-01-01"),
|
|
||||||
"volatility_scale": 0.69, # Slight increase
|
|
||||||
"trend_scale": 0.80,
|
|
||||||
"skew_scale": 1.0,
|
|
||||||
},
|
|
||||||
"mature": {
|
|
||||||
"start_date": pd.Timestamp("2020-01-01"),
|
|
||||||
"end_date": pd.Timestamp("2100-01-01"),
|
|
||||||
"volatility_scale": 0.67, # Slight increase
|
|
||||||
"trend_scale": 0.85,
|
|
||||||
"skew_scale": 1.0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def adjust_trend_expectations(expected_returns, cycle_position):
|
def adjust_trend_expectations(expected_returns, cycle_position):
|
||||||
@@ -518,10 +693,10 @@ def calculate_confidence_intervals(simulated_paths, confidence_levels=[0.95, 0.6
|
|||||||
def project_prices(
|
def project_prices(
|
||||||
df, days_forward=365, simulations=1000, confidence_levels=[0.95, 0.68]
|
df, days_forward=365, simulations=1000, confidence_levels=[0.95, 0.68]
|
||||||
):
|
):
|
||||||
"""
|
"""Generate price projections with fundamental-based adjustments."""
|
||||||
Modified projection function incorporating enhanced uncertainty estimation.
|
|
||||||
"""
|
|
||||||
df = df.copy()
|
df = df.copy()
|
||||||
|
fundamentals = MarketFundamentals()
|
||||||
|
|
||||||
df["Log_Price"] = np.log(df["Close"])
|
df["Log_Price"] = np.log(df["Close"])
|
||||||
df["Log_Return"] = df["Log_Price"].diff()
|
df["Log_Return"] = df["Log_Price"].diff()
|
||||||
|
|
||||||
@@ -535,43 +710,24 @@ def project_prices(
|
|||||||
last_price = df["Close"].iloc[-1]
|
last_price = df["Close"].iloc[-1]
|
||||||
last_date = df["Date"].iloc[-1]
|
last_date = df["Date"].iloc[-1]
|
||||||
|
|
||||||
# Generate dates for projection
|
# Generate projection dates
|
||||||
future_dates = pd.date_range(
|
future_dates = pd.date_range(
|
||||||
start=last_date + timedelta(days=1), periods=days_forward, freq="D"
|
start=last_date + timedelta(days=1), periods=days_forward, freq="D"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate expected returns with cycle boundary handling
|
# Calculate expected returns
|
||||||
future_cycle_days = [
|
future_cycle_days = [
|
||||||
(current_cycle_days + i) % (4 * 365) for i in range(days_forward)
|
(current_cycle_days + i) % (4 * 365) for i in range(days_forward)
|
||||||
]
|
]
|
||||||
cycle_trends = analyze_trends(df)
|
cycle_trends = analyze_trends(df)
|
||||||
|
|
||||||
# Get base expected returns
|
|
||||||
expected_returns = np.array(
|
expected_returns = np.array(
|
||||||
[cycle_trends.get(day, cycle_trends.mean()) for day in future_cycle_days]
|
[cycle_trends.get(day, cycle_trends.mean()) for day in future_cycle_days]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply trend adjustments
|
|
||||||
expected_returns = adjust_trend_expectations(expected_returns, cycle_position)
|
|
||||||
|
|
||||||
# Calculate base volatility
|
# Calculate base volatility
|
||||||
base_volatility = calculate_volatility(df)
|
base_volatility = calculate_volatility(df)
|
||||||
|
|
||||||
# Get era adjustments
|
# Get projection adjustments
|
||||||
current_era = None
|
|
||||||
for era, params in era_adjustments.items():
|
|
||||||
if (
|
|
||||||
df["Date"].min() >= params["start_date"]
|
|
||||||
and df["Date"].min() < params["end_date"]
|
|
||||||
):
|
|
||||||
current_era = era
|
|
||||||
break
|
|
||||||
if current_era is None:
|
|
||||||
current_era = "mature"
|
|
||||||
|
|
||||||
era_params = era_adjustments[current_era]
|
|
||||||
|
|
||||||
# Get projection adjustments with market awareness
|
|
||||||
projection_adjustments = get_projection_adjustments(
|
projection_adjustments = get_projection_adjustments(
|
||||||
days_forward, cycle_position, df
|
days_forward, cycle_position, df
|
||||||
)
|
)
|
||||||
@@ -581,30 +737,39 @@ def project_prices(
|
|||||||
simulated_paths = np.zeros((days_forward, simulations))
|
simulated_paths = np.zeros((days_forward, simulations))
|
||||||
|
|
||||||
for sim in range(simulations):
|
for sim in range(simulations):
|
||||||
# Apply era-specific adjustments
|
drift = expected_returns
|
||||||
drift = expected_returns * era_params["trend_scale"]
|
vol = base_volatility
|
||||||
vol = base_volatility * era_params["volatility_scale"]
|
|
||||||
|
|
||||||
# Scale volatility by projection adjustments
|
|
||||||
time_scaled_vol = vol * projection_adjustments
|
time_scaled_vol = vol * projection_adjustments
|
||||||
|
|
||||||
# Generate returns with time-varying volatility
|
|
||||||
returns = np.random.normal(loc=drift, scale=time_scaled_vol, size=days_forward)
|
returns = np.random.normal(loc=drift, scale=time_scaled_vol, size=days_forward)
|
||||||
|
|
||||||
# Calculate price path
|
|
||||||
cumulative_returns = np.cumsum(returns)
|
cumulative_returns = np.cumsum(returns)
|
||||||
price_path = last_price * np.exp(cumulative_returns)
|
price_path = last_price * np.exp(cumulative_returns)
|
||||||
simulated_paths[:, sim] = price_path
|
simulated_paths[:, sim] = price_path
|
||||||
|
|
||||||
# Calculate results with dynamic confidence intervals
|
# Calculate results
|
||||||
results = pd.DataFrame(index=future_dates)
|
results = pd.DataFrame(index=future_dates)
|
||||||
results["Median"] = np.percentile(simulated_paths, 50, axis=1)
|
results["Median"] = np.percentile(simulated_paths, 50, axis=1)
|
||||||
results["Expected_Trend"] = last_price * np.exp(np.cumsum(drift))
|
results["Expected_Trend"] = last_price * np.exp(np.cumsum(drift))
|
||||||
|
|
||||||
# Calculate confidence intervals with dynamic adjustment
|
# Calculate confidence intervals
|
||||||
ci_results = calculate_confidence_intervals(simulated_paths, confidence_levels)
|
for level in confidence_levels:
|
||||||
for key, values in ci_results.items():
|
# Get market metrics for confidence interval adjustment
|
||||||
results[key] = values
|
metrics = fundamentals.get_market_maturity_metrics(df, current_date)
|
||||||
|
maturity_adjustment = np.clip(metrics["market_depth"], 0, 0.5)
|
||||||
|
|
||||||
|
# Calculate adjusted confidence level
|
||||||
|
effective_level = level + (1 - level) * maturity_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
|
return results
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user