Implement some basic backtesting.
This commit is contained in:
@@ -70,6 +70,9 @@ def get_nice_price_points(min_price, max_price):
|
|||||||
Generate a reasonable set of price points for the y-axis that look clean
|
Generate a reasonable set of price points for the y-axis that look clean
|
||||||
and cover the range without cluttering the chart.
|
and cover the range without cluttering the chart.
|
||||||
"""
|
"""
|
||||||
|
# Handle zero or negative prices
|
||||||
|
min_price = max(min_price, 0.0001) # Set minimum price to $0.0001
|
||||||
|
|
||||||
log_min = np.floor(np.log10(min_price))
|
log_min = np.floor(np.log10(min_price))
|
||||||
log_max = np.ceil(np.log10(max_price))
|
log_max = np.ceil(np.log10(max_price))
|
||||||
price_points = []
|
price_points = []
|
||||||
@@ -708,6 +711,206 @@ def visualize_cycle_patterns(df, cycle_returns, cycle_volatility):
|
|||||||
plt.close()
|
plt.close()
|
||||||
|
|
||||||
|
|
||||||
|
def create_backtest_plot(
|
||||||
|
df, backtest_date="2020-05-11", start_date="2012-11-28", project_days=1650
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a plot comparing actual price history against model projections from a historical date.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame with historical price data
|
||||||
|
backtest_date: Date to start the backtest from (default: third halving)
|
||||||
|
start_date: Date to start considering historical data (default: first halving)
|
||||||
|
project_days: Number of days to project forward from backtest date
|
||||||
|
"""
|
||||||
|
# Convert dates to datetime
|
||||||
|
backtest_date = pd.to_datetime(backtest_date)
|
||||||
|
start_date = pd.to_datetime(start_date)
|
||||||
|
|
||||||
|
# Validate dates
|
||||||
|
if start_date >= backtest_date:
|
||||||
|
raise ValueError("start_date must be earlier than backtest_date")
|
||||||
|
|
||||||
|
# Clean the data: remove rows with zero or invalid prices and filter by date
|
||||||
|
df = df[(df["Close"] > 0) & (df["Date"] >= start_date)].copy()
|
||||||
|
|
||||||
|
# Split data into training (before backtest date) and validation (after backtest date)
|
||||||
|
training_df = df[df["Date"] <= backtest_date].copy()
|
||||||
|
validation_df = df[df["Date"] > backtest_date].copy()
|
||||||
|
|
||||||
|
# Check if we have enough data
|
||||||
|
if len(training_df) < 30: # Require at least 30 days of training data
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up the plot
|
||||||
|
plt.style.use("seaborn-v0_8")
|
||||||
|
fig, ax = plt.figure(figsize=(15, 10)), plt.gca()
|
||||||
|
|
||||||
|
# Plot training data
|
||||||
|
ax.semilogy(
|
||||||
|
training_df["Date"],
|
||||||
|
training_df["Close"],
|
||||||
|
"b-",
|
||||||
|
label=f'Historical Price (Training: {start_date.strftime("%Y-%m-%d")} to {backtest_date.strftime("%Y-%m-%d")})',
|
||||||
|
alpha=0.7,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Plot validation data
|
||||||
|
ax.semilogy(
|
||||||
|
validation_df["Date"],
|
||||||
|
validation_df["Close"],
|
||||||
|
"g-",
|
||||||
|
label=f'Actual Price (Validation: {backtest_date.strftime("%Y-%m-%d")} onwards)',
|
||||||
|
linewidth=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Plot projections
|
||||||
|
ax.semilogy(
|
||||||
|
historical_projections.index,
|
||||||
|
historical_projections["Expected_Trend"],
|
||||||
|
"--",
|
||||||
|
color="purple",
|
||||||
|
label="Model Projection (Expected)",
|
||||||
|
)
|
||||||
|
ax.semilogy(
|
||||||
|
historical_projections.index,
|
||||||
|
historical_projections["Median"],
|
||||||
|
":",
|
||||||
|
color="orange",
|
||||||
|
label="Model Projection (Median)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add confidence intervals
|
||||||
|
ax.fill_between(
|
||||||
|
historical_projections.index,
|
||||||
|
historical_projections["Lower_95"],
|
||||||
|
historical_projections["Upper_95"],
|
||||||
|
alpha=0.2,
|
||||||
|
color="orange",
|
||||||
|
label="95% Confidence Interval",
|
||||||
|
)
|
||||||
|
ax.fill_between(
|
||||||
|
historical_projections.index,
|
||||||
|
historical_projections["Lower_68"],
|
||||||
|
historical_projections["Upper_68"],
|
||||||
|
alpha=0.3,
|
||||||
|
color="green",
|
||||||
|
label="68% Confidence Interval",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Customize y-axis
|
||||||
|
ax.yaxis.set_major_formatter(plt.FuncFormatter(format_price))
|
||||||
|
|
||||||
|
# Set custom y-axis ticks
|
||||||
|
min_price = min(
|
||||||
|
df["Low"].min(),
|
||||||
|
historical_projections["Lower_95"].min(),
|
||||||
|
0.0001, # Set minimum price floor
|
||||||
|
)
|
||||||
|
max_price = max(df["High"].max(), historical_projections["Upper_95"].max())
|
||||||
|
price_points = get_nice_price_points(min_price, max_price)
|
||||||
|
ax.set_yticks(price_points)
|
||||||
|
|
||||||
|
# Add halving lines
|
||||||
|
halving_dates = get_halving_dates()
|
||||||
|
relevant_halvings = halving_dates[
|
||||||
|
(halving_dates >= start_date) & (halving_dates <= validation_df["Date"].max())
|
||||||
|
]
|
||||||
|
for date in relevant_halvings:
|
||||||
|
ax.axvline(date, color="red", linestyle="--", alpha=0.3)
|
||||||
|
ax.text(
|
||||||
|
date,
|
||||||
|
ax.get_ylim()[1],
|
||||||
|
"Halving",
|
||||||
|
rotation=90,
|
||||||
|
va="top",
|
||||||
|
ha="right",
|
||||||
|
alpha=0.7,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate and add model performance metrics
|
||||||
|
if len(validation_df) > 0:
|
||||||
|
# Create a common date range for comparison
|
||||||
|
actual_prices = validation_df.set_index("Date")["Close"]
|
||||||
|
common_dates = actual_prices.index.intersection(historical_projections.index)
|
||||||
|
|
||||||
|
if len(common_dates) > 0:
|
||||||
|
actual_aligned = actual_prices[common_dates]
|
||||||
|
projections_aligned = historical_projections.loc[common_dates]
|
||||||
|
|
||||||
|
# Calculate metrics using aligned data
|
||||||
|
mape = (
|
||||||
|
np.mean(
|
||||||
|
np.abs(
|
||||||
|
(actual_aligned - projections_aligned["Expected_Trend"])
|
||||||
|
/ actual_aligned
|
||||||
|
)
|
||||||
|
)
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
coverage_95 = (
|
||||||
|
np.mean(
|
||||||
|
(actual_aligned >= projections_aligned["Lower_95"])
|
||||||
|
& (actual_aligned <= projections_aligned["Upper_95"])
|
||||||
|
)
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
coverage_68 = (
|
||||||
|
np.mean(
|
||||||
|
(actual_aligned >= projections_aligned["Lower_68"])
|
||||||
|
& (actual_aligned <= projections_aligned["Upper_68"])
|
||||||
|
)
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
rmse = np.sqrt(
|
||||||
|
np.mean((actual_aligned - projections_aligned["Expected_Trend"]) ** 2)
|
||||||
|
)
|
||||||
|
max_error = np.max(
|
||||||
|
np.abs(actual_aligned - projections_aligned["Expected_Trend"])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add metrics to plot
|
||||||
|
metrics_text = (
|
||||||
|
f"Model Performance Metrics:\n"
|
||||||
|
f"MAPE: {mape:.1f}%\n"
|
||||||
|
f"RMSE: ${rmse:,.0f}\n"
|
||||||
|
f"Max Error: ${max_error:,.0f}\n"
|
||||||
|
f"95% CI Coverage: {coverage_95:.1f}%\n"
|
||||||
|
f"68% CI Coverage: {coverage_68:.1f}%"
|
||||||
|
)
|
||||||
|
ax.text(
|
||||||
|
0.02,
|
||||||
|
0.98,
|
||||||
|
metrics_text,
|
||||||
|
transform=ax.transAxes,
|
||||||
|
verticalalignment="top",
|
||||||
|
bbox=dict(facecolor="white", alpha=0.8),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Customize plot
|
||||||
|
ax.set_title(
|
||||||
|
f'Bitcoin Price: Model Backtest\nTraining: {start_date.strftime("%Y-%m-%d")} to {backtest_date.strftime("%Y-%m-%d")}'
|
||||||
|
)
|
||||||
|
ax.set_xlabel("Date")
|
||||||
|
ax.set_ylabel("Price (USD)")
|
||||||
|
ax.grid(True, which="major", linestyle="-", alpha=0.5)
|
||||||
|
ax.grid(True, which="minor", linestyle=":", alpha=0.2)
|
||||||
|
ax.legend(loc="center left", bbox_to_anchor=(1.02, 0.5))
|
||||||
|
|
||||||
|
# Adjust layout and save
|
||||||
|
plt.tight_layout()
|
||||||
|
filename = f'bitcoin_backtest_{start_date.strftime("%Y%m%d")}_to_{backtest_date.strftime("%Y%m%d")}.png'
|
||||||
|
plt.savefig(filename, dpi=300, bbox_inches="tight")
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
return historical_projections
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
analysis, df = analyze_bitcoin_prices("prices.csv")
|
analysis, df = analyze_bitcoin_prices("prices.csv")
|
||||||
# create_plots(df) # Full history
|
# create_plots(df) # Full history
|
||||||
@@ -717,3 +920,59 @@ if __name__ == "__main__":
|
|||||||
projections = create_plots(df, start="2011-01-01", project_days=365 * 4)
|
projections = create_plots(df, start="2011-01-01", project_days=365 * 4)
|
||||||
print("\nProjected Prices at Key Points:")
|
print("\nProjected Prices at Key Points:")
|
||||||
print(projections.iloc[[29, 89, 179, 364]].round(2)) # 30, 90, 180, 365 days
|
print(projections.iloc[[29, 89, 179, 364]].round(2)) # 30, 90, 180, 365 days
|
||||||
|
|
||||||
|
# Create multiple backtests for different periods
|
||||||
|
backtests = [
|
||||||
|
# First to second halving
|
||||||
|
{
|
||||||
|
"start_date": "2012-11-28", # First halving
|
||||||
|
"backtest_date": "2016-07-09", # Second halving
|
||||||
|
"project_days": 1460, # 4 years
|
||||||
|
},
|
||||||
|
# Second to third halving
|
||||||
|
{
|
||||||
|
"start_date": "2016-07-09", # Second halving
|
||||||
|
"backtest_date": "2020-05-11", # Third halving
|
||||||
|
"project_days": 1460, # 4 years
|
||||||
|
},
|
||||||
|
# Third halving onwards
|
||||||
|
{
|
||||||
|
"start_date": "2020-05-11", # Third halving
|
||||||
|
"backtest_date": "2024-04-19", # Fourth halving (projected)
|
||||||
|
"project_days": 1460, # 4 years
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start_date": "2016-07-09", # Second halving
|
||||||
|
"backtest_date": "2024-04-19", # Fourth halving (projected)
|
||||||
|
"project_days": 1460, # 4 years
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Run all backtests
|
||||||
|
for params in backtests:
|
||||||
|
print(
|
||||||
|
f"\nRunning backtest from {params['start_date']} to {params['backtest_date']}"
|
||||||
|
)
|
||||||
|
backtest_projections = create_backtest_plot(df, **params)
|
||||||
|
|
||||||
|
# Print some key projection points vs actual prices
|
||||||
|
print("\nBacktest Results - Projected vs Actual Prices:")
|
||||||
|
validation_df = df[df["Date"] > params["backtest_date"]]
|
||||||
|
actual_prices = validation_df.set_index("Date")["Close"]
|
||||||
|
|
||||||
|
for days in [30, 90, 180, 365]:
|
||||||
|
target_date = pd.to_datetime(params["backtest_date"]) + pd.Timedelta(
|
||||||
|
days=days
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
target_date in actual_prices.index
|
||||||
|
and target_date in backtest_projections.index
|
||||||
|
):
|
||||||
|
projected = backtest_projections.loc[target_date]
|
||||||
|
actual = actual_prices.loc[target_date]
|
||||||
|
print(f"\n{days} days out ({target_date.strftime('%Y-%m-%d')}):")
|
||||||
|
print(f"Actual Price: ${actual:,.2f}")
|
||||||
|
print(f"Projected (Expected): ${projected['Expected_Trend']:,.2f}")
|
||||||
|
print(
|
||||||
|
f"Projected Range: ${projected['Lower_95']:,.2f} - ${projected['Upper_95']:,.2f}"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user