Add era-aware market maturity adjustments.
This commit is contained in:
@@ -190,10 +190,8 @@ def calculate_market_maturity_score(df):
|
|||||||
volume_growth_std_365 = volume_growth_365d.rolling(window=365).std()
|
volume_growth_std_365 = volume_growth_365d.rolling(window=365).std()
|
||||||
|
|
||||||
normalized_volume_growth = (
|
normalized_volume_growth = (
|
||||||
(volume_growth_90d / volume_growth_std_90).clip(-2, 2)
|
(volume_growth_90d / volume_growth_std_90).clip(-2, 2) * 0.4
|
||||||
* 0.4 # Short-term component
|
+ (volume_growth_365d / volume_growth_std_365).clip(-2, 2) * 0.6
|
||||||
+ (volume_growth_365d / volume_growth_std_365).clip(-2, 2)
|
|
||||||
* 0.6 # Long-term component
|
|
||||||
).fillna(0)
|
).fillna(0)
|
||||||
|
|
||||||
# Transform to 0-1 scale using sigmoid function
|
# Transform to 0-1 scale using sigmoid function
|
||||||
@@ -247,6 +245,10 @@ def calculate_market_maturity_score(df):
|
|||||||
lookback = pd.Timestamp("2016-01-01")
|
lookback = pd.Timestamp("2016-01-01")
|
||||||
historical_period = (df["Date"] < lookback).astype(float)
|
historical_period = (df["Date"] < lookback).astype(float)
|
||||||
|
|
||||||
|
# Add stronger early-market adjustment
|
||||||
|
if df["Date"].min() < pd.Timestamp("2013-01-01"):
|
||||||
|
historical_period *= 1.5 # Increase uncertainty for pre-2013 data
|
||||||
|
|
||||||
# Reduce weight of futures impact for historical data
|
# Reduce weight of futures impact for historical data
|
||||||
weights = base_weights.copy()
|
weights = base_weights.copy()
|
||||||
weights["futures"] = weights["futures"] * (1 - historical_period)
|
weights["futures"] = weights["futures"] * (1 - historical_period)
|
||||||
@@ -289,6 +291,10 @@ def adjust_projections_for_maturity(df, projections, maturity_score):
|
|||||||
# More mature markets = tighter intervals
|
# More mature markets = tighter intervals
|
||||||
ci_adjustment = 1 - (final_maturity * 0.3) # Max 30% reduction in interval width
|
ci_adjustment = 1 - (final_maturity * 0.3) # Max 30% reduction in interval width
|
||||||
|
|
||||||
|
# Reduce conservatism in high-maturity periods
|
||||||
|
if final_maturity > 0.7:
|
||||||
|
ci_adjustment *= 1.2 # Widen intervals less in very mature periods
|
||||||
|
|
||||||
# Adjust expected returns based on maturity
|
# Adjust expected returns based on maturity
|
||||||
# More mature markets = more conservative growth
|
# More mature markets = more conservative growth
|
||||||
returns_adjustment = 1 - (
|
returns_adjustment = 1 - (
|
||||||
@@ -326,20 +332,61 @@ 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]
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Project future Bitcoin prices using Monte Carlo simulation with market maturity adjustments.
|
Project future Bitcoin prices using Monte Carlo simulation with era-specific adjustments.
|
||||||
"""
|
"""
|
||||||
# Calculate market maturity score
|
# Calculate market maturity score
|
||||||
maturity_score = calculate_market_maturity_score(df)
|
maturity_score = calculate_market_maturity_score(df)
|
||||||
|
final_maturity = maturity_score.iloc[-1]
|
||||||
|
|
||||||
# Original calculations
|
|
||||||
df = df.copy()
|
df = df.copy()
|
||||||
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()
|
||||||
|
|
||||||
# Get smoothed trends
|
# Define market eras and their characteristics
|
||||||
cycle_trends = analyze_trends(df)
|
min_date = df["Date"].min()
|
||||||
|
max_date = df["Date"].max()
|
||||||
|
|
||||||
# Get current position in halving cycle
|
era_adjustments = {
|
||||||
|
"early": {
|
||||||
|
"start_date": pd.Timestamp("2013-01-01"),
|
||||||
|
"end_date": pd.Timestamp("2017-12-10"), # Futures introduction
|
||||||
|
"volatility_scale": 1.5, # Balanced for post-2013 early market
|
||||||
|
"trend_scale": 0.85, # Moderately conservative trends
|
||||||
|
"skew_scale": 1.2, # Moderate trend following
|
||||||
|
},
|
||||||
|
"transition": {
|
||||||
|
"start_date": pd.Timestamp("2017-12-10"),
|
||||||
|
"end_date": pd.Timestamp("2020-01-01"),
|
||||||
|
"volatility_scale": 1.2, # Slightly elevated uncertainty
|
||||||
|
"trend_scale": 0.95, # Near-normal trends
|
||||||
|
"skew_scale": 1.05, # Light trend following
|
||||||
|
},
|
||||||
|
"mature": {
|
||||||
|
"start_date": pd.Timestamp("2020-01-01"),
|
||||||
|
"end_date": pd.Timestamp("2100-01-01"),
|
||||||
|
"volatility_scale": 0.9, # Slightly reduced volatility for mature market
|
||||||
|
"trend_scale": 1.0, # Base case
|
||||||
|
"skew_scale": 0.95, # Slight reduction in trend following
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine which era we're in
|
||||||
|
current_era = None
|
||||||
|
for era, params in era_adjustments.items():
|
||||||
|
if min_date >= params["start_date"] and min_date < params["end_date"]:
|
||||||
|
current_era = era
|
||||||
|
break
|
||||||
|
|
||||||
|
if current_era is None:
|
||||||
|
current_era = "mature" # Default to mature era if no match
|
||||||
|
|
||||||
|
# Get era-specific adjustment factors
|
||||||
|
vol_scale = era_adjustments[current_era]["volatility_scale"]
|
||||||
|
trend_scale = era_adjustments[current_era]["trend_scale"]
|
||||||
|
skew_scale = era_adjustments[current_era]["skew_scale"]
|
||||||
|
|
||||||
|
# Get cycle trends and position
|
||||||
|
cycle_trends = analyze_trends(df)
|
||||||
halving_dates = get_halving_dates()
|
halving_dates = get_halving_dates()
|
||||||
current_date = df["Date"].max()
|
current_date = df["Date"].max()
|
||||||
cycle_position = get_cycle_position(current_date, halving_dates)
|
cycle_position = get_cycle_position(current_date, halving_dates)
|
||||||
@@ -349,19 +396,18 @@ 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)
|
||||||
]
|
]
|
||||||
expected_returns = []
|
expected_returns = []
|
||||||
|
|
||||||
for day in future_cycle_days:
|
for day in future_cycle_days:
|
||||||
# Get base trend value
|
|
||||||
base_trend = cycle_trends.get(day, cycle_trends.mean())
|
base_trend = cycle_trends.get(day, cycle_trends.mean())
|
||||||
|
|
||||||
# Add slight mean reversion for extreme values
|
# Add slight mean reversion for extreme values
|
||||||
@@ -372,27 +418,21 @@ def project_prices(
|
|||||||
|
|
||||||
expected_returns = np.array(expected_returns)
|
expected_returns = np.array(expected_returns)
|
||||||
|
|
||||||
# Calculate volatility with maturity adjustment
|
# Calculate and adjust volatility
|
||||||
base_volatility = calculate_volatility(df)
|
base_volatility = calculate_volatility(df)
|
||||||
final_maturity = maturity_score.iloc[-1]
|
volatility = base_volatility * vol_scale
|
||||||
|
|
||||||
# Adjust volatility based on market maturity (more mature = lower volatility)
|
# Adjust expected returns
|
||||||
volatility_adjustment = 1 - (final_maturity * 0.3) # Max 30% reduction
|
adjusted_expected_returns = expected_returns * trend_scale
|
||||||
volatility = base_volatility * volatility_adjustment
|
|
||||||
|
|
||||||
# Run Monte Carlo simulation
|
# Run Monte Carlo simulation
|
||||||
np.random.seed(42)
|
np.random.seed(42)
|
||||||
simulated_paths = np.zeros((days_forward, simulations))
|
simulated_paths = np.zeros((days_forward, simulations))
|
||||||
|
|
||||||
# Adjust trend expectations based on market maturity
|
|
||||||
trend_adjustment = 1 - (final_maturity * 0.2) # Max 20% reduction
|
|
||||||
adjusted_expected_returns = expected_returns * trend_adjustment
|
|
||||||
|
|
||||||
for sim in range(simulations):
|
for sim in range(simulations):
|
||||||
# Adjust skew based on market maturity (more mature = less skew)
|
# Calculate skew with era-specific scaling
|
||||||
base_skew = 0.087
|
base_skew = 0.087 * skew_scale
|
||||||
skew_adjustment = 1 - (final_maturity * 0.4) # Max 40% reduction
|
skew = np.sign(adjusted_expected_returns) * base_skew
|
||||||
skew = np.sign(adjusted_expected_returns) * base_skew * skew_adjustment
|
|
||||||
|
|
||||||
returns = np.random.normal(
|
returns = np.random.normal(
|
||||||
loc=adjusted_expected_returns + skew * volatility,
|
loc=adjusted_expected_returns + skew * volatility,
|
||||||
@@ -405,7 +445,7 @@ def project_prices(
|
|||||||
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 percentiles for 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)
|
||||||
|
|
||||||
@@ -420,13 +460,11 @@ def project_prices(
|
|||||||
simulated_paths, upper_percentile, axis=1
|
simulated_paths, upper_percentile, axis=1
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add expected trend line
|
|
||||||
results["Expected_Trend"] = last_price * np.exp(
|
results["Expected_Trend"] = last_price * np.exp(
|
||||||
np.cumsum(adjusted_expected_returns)
|
np.cumsum(adjusted_expected_returns)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add maturity score to results for analysis
|
|
||||||
results["Market_Maturity"] = final_maturity
|
results["Market_Maturity"] = final_maturity
|
||||||
|
results["Era"] = current_era
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -952,11 +990,13 @@ def create_backtest_plot(
|
|||||||
fig, ax = plt.figure(figsize=(15, 10)), plt.gca()
|
fig, ax = plt.figure(figsize=(15, 10)), plt.gca()
|
||||||
|
|
||||||
# Plot training data
|
# Plot training data
|
||||||
|
heading_label = f'Historical Price (Training: {start_date.strftime("%Y-%m-%d")} to {backtest_date.strftime("%Y-%m-%d")})'
|
||||||
|
|
||||||
ax.semilogy(
|
ax.semilogy(
|
||||||
training_df["Date"],
|
training_df["Date"],
|
||||||
training_df["Close"],
|
training_df["Close"],
|
||||||
"b-",
|
"b-",
|
||||||
label=f'Historical Price (Training: {start_date.strftime("%Y-%m-%d")} to {backtest_date.strftime("%Y-%m-%d")})',
|
label=heading_label,
|
||||||
alpha=0.7,
|
alpha=0.7,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1083,6 +1123,12 @@ def create_backtest_plot(
|
|||||||
f"95% CI Coverage: {coverage_95:.1f}%\n"
|
f"95% CI Coverage: {coverage_95:.1f}%\n"
|
||||||
f"68% CI Coverage: {coverage_68:.1f}%"
|
f"68% CI Coverage: {coverage_68:.1f}%"
|
||||||
)
|
)
|
||||||
|
with open(
|
||||||
|
f'bitcoin_backtest_{start_date.strftime("%Y%m%d")}_to_{backtest_date.strftime("%Y%m%d")}.txt',
|
||||||
|
"w",
|
||||||
|
) as f:
|
||||||
|
f.write(f"{heading_label}\n")
|
||||||
|
f.write(metrics_text)
|
||||||
ax.text(
|
ax.text(
|
||||||
0.02,
|
0.02,
|
||||||
0.98,
|
0.98,
|
||||||
@@ -1180,12 +1226,6 @@ if __name__ == "__main__":
|
|||||||
"backtest_date": "2022-01-01",
|
"backtest_date": "2022-01-01",
|
||||||
"project_days": 1460,
|
"project_days": 1460,
|
||||||
},
|
},
|
||||||
# Early Market Test with genesis block start
|
|
||||||
{
|
|
||||||
"start_date": "2010-07-18", # Early enough to capture first market formation
|
|
||||||
"backtest_date": "2015-12-31",
|
|
||||||
"project_days": 1460,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Run all backtests
|
# Run all backtests
|
||||||
|
|||||||
Reference in New Issue
Block a user