by Hans Darmawan
Bitcoin price data pose a uniquely challenging time-series problem: they are highly volatile, non-stationary, regime-driven, and structurally incomplete, with missing calendar days and rapidly changing dynamics. Despite this complexity, many applied analyses jump directly to sophisticated models without first establishing strong, interpretable baselines or validating the temporal structure of the data. This project is interesting because it deliberately addresses that gap—treating Bitcoin as a long-horizon financial time series rather than a trading signal, enforcing explicit time indexing, and rigorously evaluating whether added model complexity truly improves short-term forecasts. By showing that simple benchmarks can rival or outperform more complex methods, the analysis challenges common assumptions in crypto modeling and provides a principled foundation for more advanced time-series research.
This section initializes all required R packages for time series analysis, data manipulation, and visualization.
Loading libraries at the beginning guarantees reproducibility and avoids namespace conflicts later in the analysis.
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.6
## ✔ forcats 1.0.1 ✔ stringr 1.6.0
## ✔ ggplot2 4.0.1 ✔ tibble 3.3.0
## ✔ lubridate 1.9.4 ✔ tidyr 1.3.2
## ✔ purrr 1.2.0
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(tsibble)
## Registered S3 method overwritten by 'tsibble':
## method from
## as_tibble.grouped_df dplyr
##
## Attaching package: 'tsibble'
##
## The following object is masked from 'package:lubridate':
##
## interval
##
## The following objects are masked from 'package:base':
##
## intersect, setdiff, union
library(lubridate)
library(slider)
library(fable)
## Loading required package: fabletools
library(here)
## here() starts at C:/Users/U1/Documents/bitcoin-ts-2
This section loads the daily Bitcoin price dataset from a relative
file path using the here package.
Using a relative path improves project portability and ensures the analysis runs consistently across different machines and directory structures.
The dataset is expected to contain: - A date column
representing daily timestamps - A close column representing
daily closing prices
Immediately after loading, the total number of observations is printed to verify that the data was imported correctly.
btc_raw <- read_csv(
here::here("data", "btc_2014_2025.csv"),
show_col_types = FALSE
)
message("Rows loaded (daily): ", nrow(btc_raw))
## Rows loaded (daily): 4121
The dataset was successfully loaded, containing 4,121 daily observations of Bitcoin prices.
This row count indicates:
A long historical coverage, spanning multiple market cycles (bull, bear, and consolidation phases)
Sufficient data density for time series modeling, including rolling statistics and forecasting
Adequate sample size for out-of-sample evaluation without sacrificing training depth
At a daily frequency, 4,121 observations correspond to over 11 years of data, which is appropriate for capturing long-term trends, volatility clustering, and structural changes commonly observed in cryptocurrency markets.
Overall, the data volume is well-suited for robust exploratory analysis and baseline forecasting, and provides a strong foundation for more advanced modeling extensions.
Before proceeding with analysis, basic schema validation is performed.
The code checks whether all required columns (date,
close) exist in the dataset.
If any required column is missing, the script stops execution with a
clear error message.
This defensive programming step prevents silent failures and ensures that downstream transformations and models operate on valid inputs.
required_cols <- c("date", "close")
missing_cols <- setdiff(required_cols, names(btc_raw))
if (length(missing_cols) > 0) {
stop(
"Missing required columns: ",
paste(missing_cols, collapse = ", ")
)
}
In this step, the raw dataset is transformed into a clean, structured time series object.
Key operations include: - Converting date to a proper
Date class - Casting close prices to numeric
values - Sorting observations chronologically - Converting the dataset
into a tsibble using date as the time
index
The resulting tsibble explicitly encodes temporal
structure, enabling time-aware operations such as forecasting, gap
detection, and rolling statistics.
btc_ts <- btc_raw %>%
mutate(
date = as.Date(date),
close = as.numeric(close)
) %>%
arrange(date) %>%
as_tsibble(index = date)
btc_ts
## # A tsibble: 4,121 x 6 [1D]
## date open high low close volume
## <date> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2014-09-17 466. 468. 452. 457. 21056800
## 2 2014-09-18 457. 457. 413. 424. 34483200
## 3 2014-09-19 424. 428. 385. 395. 37919700
## 4 2014-09-20 395. 423. 390. 409. 36863600
## 5 2014-09-21 408. 412. 393. 399. 26580100
## 6 2014-09-22 399. 407. 397. 402. 24127600
## 7 2014-09-23 402. 442. 396. 436. 45099500
## 8 2014-09-24 436. 436. 421. 423. 30627700
## 9 2014-09-25 423. 424. 409. 412. 26814400
## 10 2014-09-26 411. 415. 400. 404. 21460800
## # ℹ 4,111 more rows
The raw Bitcoin dataset was successfully cleaned and converted into a daily tsibble with 4,121 observations and 6 variables, indexed explicitly by date.
Key confirmations from the output: - The time index
(date) is correctly parsed as a Date class and
ordered chronologically. - Price-related fields (open,
high, low, close) and
volume are stored as numeric (<dbl>),
ensuring compatibility with statistical modeling. - The interval
[1D] confirms a regular daily frequency,
which is essential for rolling-window calculations and forecasting.
The presence of OHLC and volume data indicates flexibility for future extensions beyond closing-price analysis, such as: - Volatility and range-based indicators - Candlestick-based features - Volume-informed regime analysis
Overall, the tsibble structure validates that the dataset is time-aware, clean, and analysis-ready, forming a reliable foundation for exploratory analysis, rolling statistics, and baseline forecasting models.
Sanity checks are performed to ensure time series integrity.
NA
values exist in the closing price column.These checks are critical for financial time series, as missing days or values can bias rolling statistics and forecasting models.
gaps <- has_gaps(btc_ts)
if (nrow(gaps) > 0) {
warning("⚠️ Time series has missing DAYS (gaps detected)")
print(gaps)
}
## Warning: ⚠️ Time series has missing DAYS (gaps detected)
## # A tibble: 1 × 1
## .gaps
## <lgl>
## 1 TRUE
if (any(is.na(btc_ts$close))) {
warning("⚠️ Missing values detected in 'close'")
}
The sanity checks indicate that the time series contains
missing calendar days, as gap detection returned
TRUE.
This result means:
The dataset is indexed at a daily frequency, but not every calendar day is present
One or more dates are missing in the sequence, likely due to:
Exchange-specific data availability
Early-market sparsity
Data source limitations rather than true market closure (Bitcoin trades 24/7)
Importantly:
No missing values were detected in the close
price column, confirming that all existing observations are
numerically complete
The issue is therefore structural (missing dates) rather than data quality (missing prices)
Implications for analysis: - Rolling-window calculations (e.g., moving averages, volatility) may be affected if gaps are not handled explicitly - Forecasting models assuming a regular daily interval should account for these gaps, either by: - Explicitly filling missing dates, or - Accepting the irregularity if supported by the modeling framework
Overall, the data remains usable and reliable, but the presence of gaps should be acknowledged and addressed deliberately in downstream time series modeling steps.
Exploratory Data Analysis (EDA) provides an initial understanding of Bitcoin’s price dynamics over time.
Visual inspection helps identify:
Long-term growth trends
Structural breaks
Periods of extreme volatility
Regime shifts commonly observed in cryptocurrency markets
EDA is a crucial step before applying any statistical or forecasting models.
This plot visualizes the entire daily Bitcoin price history from 2014 to 2025.
It highlights:
The long-term upward trend
High volatility relative to traditional assets
Distinct bull and bear market cycles
This global view provides context for subsequent focused analyses on shorter time horizons.
ggplot(btc_ts, aes(date, close)) +
geom_line(alpha = 0.6) +
theme_minimal() +
labs(
title = "Bitcoin Daily Closing Price (Full History)",
x = "Date",
y = "Price"
)
The full-history price plot reveals Bitcoin’s strong long-term upward trend, accompanied by extreme volatility and recurring boom–bust cycles.
Key observations from the visualization: - An extended low-price accumulation phase in the early years, followed by rapid exponential growth as adoption increased.
Multiple distinct bull and bear market cycles, characterized by sharp price run-ups followed by deep corrections.
Increasing price amplitude over time, indicating that both gains and drawdowns have grown larger in absolute terms as market capitalization expanded.
Clear evidence of structural breaks and regime shifts, where price dynamics change markedly across different periods.
Despite short-term turbulence, the overall trajectory shows persistent growth, suggesting that Bitcoin behaves more like a high-volatility growth asset than a stable financial instrument.
This global view provides essential context for modeling decisions, highlighting the need for:
Log-scale or return-based transformations
Models robust to non-stationarity
Caution when interpreting forecasts on raw price levels
This section zooms into the most recent two years of Bitcoin price data.
Focusing on recent history allows:
Better inspection of current market behavior
Identification of short-term trends and volatility patterns
Alignment with practical forecasting horizons used in trading and risk analysis
btc_recent <- btc_ts %>%
filter(date >= max(date) - years(2))
ggplot(btc_recent, aes(date, close)) +
geom_line(color = "steelblue") +
theme_minimal() +
labs(
title = "Bitcoin Daily Closing Price (Last 2 Years)",
x = "Date",
y = "Price"
)
The last two years of Bitcoin price data highlight pronounced short-term dynamics, marked by rapid trend changes and elevated volatility.
Key observations from the visualization:
A clear transition from consolidation to strong upward momentum, indicating periods of renewed bullish sentiment.
Frequent and sharp price swings within relatively short time spans, reflecting heightened speculative activity and sensitivity to market news.
The presence of multiple short-lived trends, where rallies are often followed by quick pullbacks rather than prolonged stable phases.
Volatility remains consistently high, even during upward movements, suggesting that risk remains elevated despite overall price appreciation.
This zoomed-in view emphasizes that, unlike the long-term trend, short-term Bitcoin price behavior is highly unstable and regime-dependent, reinforcing the importance of short forecasting horizons and robust risk-aware modeling approaches.
The 30-day rolling mean smooths short-term fluctuations and highlights medium-term price trends.
A rolling window of 30 days is commonly used in financial analysis to approximate monthly market behavior.
Overlaying the rolling mean on the raw price series helps distinguish: - Trend movements - Noise-driven daily fluctuations
btc_roll <- btc_ts %>%
mutate(
close_ma_30 = slide_dbl(
close,
mean,
.before = 29,
.complete = TRUE
)
)
ggplot(btc_roll, aes(date)) +
geom_line(aes(y = close), alpha = 0.4) +
geom_line(aes(y = close_ma_30), color = "red", linewidth = 0.8) +
theme_minimal() +
labs(
title = "Daily Price with 30-Day Rolling Mean",
x = "Date",
y = "Price"
)
## Warning: Removed 29 rows containing missing values or values outside the scale range
## (`geom_line()`).
The 30-day rolling mean smooths short-term noise and highlights the underlying medium-term trend in Bitcoin prices.
Key observations from the visualization:
The rolling mean closely follows the long-term price trajectory while filtering out daily volatility, making trend direction more interpretable.
Sustained upward and downward phases become clearer, especially during major bull and bear market cycles.
Turning points in the rolling mean often lag raw price movements, reflecting the smoothing effect of the 30-day window.
Periods where the price deviates strongly from the rolling mean correspond to heightened volatility and speculative excess.
Overall, the 30-day rolling mean provides a clearer view of Bitcoin’s medium-term momentum, serving as a useful tool for trend identification and contextualizing short-term price fluctuations.
This section computes a 30-day rolling standard deviation of Bitcoin prices as a proxy for market volatility.
Rolling volatility reveals: - Volatility clustering - Periods of market stress - Calm versus turbulent regimes
Such patterns are characteristic of financial assets and motivate more advanced volatility models in later stages (e.g., GARCH).
btc_vol <- btc_ts %>%
mutate(
vol_30 = slide_dbl(
close,
sd,
.before = 29,
.complete = TRUE
)
)
ggplot(btc_vol, aes(date, vol_30)) +
geom_line(color = "darkorange") +
theme_minimal() +
labs(
title = "30-Day Rolling Volatility (Std Dev)",
x = "Date",
y = "Volatility"
)
## Warning: Removed 29 rows containing missing values or values outside the scale range
## (`geom_line()`).
The 30-day rolling volatility plot highlights Bitcoin’s highly unstable and clustered risk dynamics over time.
Key observations from the visualization:
Volatility is very low in the early years, reflecting limited market participation and relatively stable price movements.
As Bitcoin adoption grows, volatility increases sharply, with large spikes coinciding with major bull and bear market phases.
Periods of elevated volatility tend to cluster rather than occur in isolation, a hallmark of financial time series behavior.
Extreme volatility spikes are often associated with market stress, rapid price corrections, or speculative blow-off tops.
In recent years, volatility remains structurally higher than in early periods, even during relatively calm price phases.
Overall, this pattern confirms that Bitcoin exhibits persistent volatility clustering and regime-dependent risk, motivating the use of volatility-aware models and caution when interpreting point forecasts.
Baseline models provide reference points against which more complex models can be evaluated.
In financial time series, simple models often perform surprisingly well, especially over short forecasting horizons.
Establishing baselines ensures that additional model complexity is justified by real performance gains.
The dataset is split into training and testing sets using a rolling-origin approach.
This approach respects the temporal ordering of the data and avoids information leakage from the future.
horizon <- 14
btc_train <- btc_ts %>%
filter(date <= max(date) - days(horizon))
btc_test <- btc_ts %>%
filter(date > max(date) - days(horizon))
message("Train rows: ", nrow(btc_train))
## Train rows: 4108
message("Test rows : ", nrow(btc_test))
## Test rows : 13
The dataset was split into 4,108 training observations and 13 test observations, following a time-aware train–test strategy.
This split implies:
The training set retains nearly the full historical context, allowing models to learn long-term trends, market cycles, and volatility regimes.
The test set represents a short and realistic forecasting horizon, which is appropriate for evaluating near-term predictive performance in financial markets.
Temporal ordering is strictly preserved, preventing information leakage from future data into the training process.
Overall, this configuration balances model learning depth with practical forecast evaluation, aligning well with short-horizon forecasting use cases commonly found in cryptocurrency analysis.
Three baseline forecasting models are fitted on the training data:
These models represent increasing levels of statistical sophistication while remaining interpretable.
models <- btc_train %>%
model(
naive = NAIVE(close),
drift = RW(close ~ drift()),
arima = ARIMA(close)
)
report(models)
## Warning in report.mdl_df(models): Model reporting is only supported for
## individual models, so a glance will be shown. To see the report for a specific
## model, use `select()` and `filter()` to identify a single model.
## # A tibble: 3 × 8
## .model sigma2 log_lik AIC AICc BIC ar_roots ma_roots
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <list> <list>
## 1 naive 1214937. NA NA NA NA <NULL> <NULL>
## 2 drift 1214937. NA NA NA NA <NULL> <NULL>
## 3 arima 1213359. -34594. 69193. 69193. 69205. <cpl [0]> <cpl [1]>
The model summary compares three baseline forecasting approaches fitted to the training data.
Key observations from the output:
The Naive and Random Walk with
Drift models report identical variance estimates
(sigma2) and do not provide likelihood-based statistics
(log-likelihood, AIC, AICc, BIC), as these models are non-parametric
baselines rather than fully specified statistical models.
Both baseline models show <NULL> values for AR
and MA roots, confirming that they do not model autocorrelation
structure explicitly and rely solely on the last observed value (with or
without drift).
The ARIMA model reports a slightly lower
estimated variance (sigma2) compared to the naive
baselines, indicating a marginally better fit to historical price
dynamics.
The presence of valid log-likelihood, AIC, AICc, and BIC values for the ARIMA model allows formal model comparison and reflects its parametric nature.
The ARIMA specification includes MA roots but no AR roots, suggesting that short-term shock effects dominate the fitted dependence structure rather than persistent autoregressive behavior.
Overall, this output confirms that:
Simple baseline models serve as useful reference points but lack statistical diagnostics.
The ARIMA model captures additional structure in the data, albeit with only modest improvement, which is typical for highly volatile and non-stationary financial price series.
Each fitted model generates forecasts for the 14-day horizon.
Forecasts are visualized together with historical data to: - Compare model behavior - Inspect forecast uncertainty - Assess plausibility of predicted trajectories
Visualization is especially important in financial contexts where numerical accuracy alone may be misleading.
fc <- models %>%
forecast(h = horizon)
autoplot(fc, btc_ts) +
theme_minimal() +
labs(
title = "Daily Bitcoin Forecast (Baseline Models)",
x = "Date",
y = "Price"
)
Three baseline models were successfully fitted and compared: Naive, Random Walk with Drift, and ARIMA.
Key observations from the model summary:
The Naive and Drift models
report identical residual variance (sigma² ≈ 1,214,937) and
do not provide likelihood-based metrics (AIC, BIC), which is expected
given their simplicity and non-parametric nature.
The ARIMA model yields a slightly lower residual
variance (sigma² ≈ 1,213,359), indicating a marginally
better in-sample fit compared to the simpler baselines.
Only the ARIMA model reports valid log-likelihood and information criteria (AIC, AICc, BIC), allowing formal statistical comparison and model selection.
The presence of a valid MA root confirms that the ARIMA model captures short-term dependence structure in the data, while the absence of AR roots suggests limited autoregressive dynamics at the chosen differencing level.
From the forecast visualization:
All three models produce very similar short-horizon forecasts, reflecting the dominance of recent price information in near-term prediction.
The Naive forecast remains flat, anchoring predictions at the last observed price.
The Drift model introduces a slight directional movement, extrapolating recent average trends.
The ARIMA forecast closely tracks the drift behavior but adds wider and more realistic uncertainty intervals, reflecting modeled residual structure.
Prediction intervals widen rapidly, highlighting the high uncertainty inherent in short-term Bitcoin price forecasting.
Overall, these results demonstrate that:
Simple baseline models already capture most short-horizon predictive power.
Increased model complexity (ARIMA) provides only modest gains in fit.
These baselines establish a strong benchmark for evaluating more advanced models such as return-based ARIMA, GARCH, or regime-switching approaches.
Forecast accuracy is evaluated using out-of-sample test data.
Common error metrics include:
MAE (Mean Absolute Error)
RMSE (Root Mean Squared Error)
MAPE (Mean Absolute Percentage Error)
Evaluating models on unseen data provides an unbiased estimate of real-world forecasting performance.
acc <- accuracy(fc, btc_test)
## Warning: The future dataset is incomplete, incomplete out-of-sample data will be treated as missing.
## 1 observation is missing at 2025-12-28
acc
## # A tibble: 3 × 10
## .model .type ME RMSE MAE MPE MAPE MASE RMSSE ACF1
## <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 arima Test 1063. 1371. 1276. 1.20 1.45 NaN NaN 0.393
## 2 drift Test 990. 1304. 1196. 1.12 1.36 NaN NaN 0.371
## 3 naive Test 1138. 1430. 1328. 1.29 1.51 NaN NaN 0.393
The model evaluation results compare baseline forecasts using out-of-sample test data, with one missing future observation handled as missing without invalidating the evaluation.
Key points from the warning:
One test observation at 2025-12-28 is missing
and was treated as NA.
The remaining test data is still sufficient for comparative model evaluation.
This situation commonly occurs near dataset boundaries and does not bias relative model performance.
Key observations from the accuracy metrics:
The Random Walk with Drift model achieves the lowest RMSE (1304.07) and lowest MAE (1195.58), indicating the strongest short-horizon predictive performance among the baselines.
The ARIMA model performs moderately well but does not outperform the drift model, suggesting that additional autoregressive complexity does not translate into better short-term accuracy on raw price levels.
The Naive model shows the highest RMSE and MAE, confirming it as a useful but weak baseline.
All models exhibit positive Mean Error (ME), indicating a tendency to underestimate actual prices during the test period.
MAPE values are relatively close across models, reflecting similar proportional error behavior in a highly volatile price regime.
MASE and RMSSE are reported as
NaN due to scaling limitations with the short test window,
which is expected and not indicative of model failure.
Overall interpretation:
The Random Walk with Drift model serves as the strongest baseline, setting a practical benchmark for future models.
The results reinforce a common finding in financial time series: simple models with drift often outperform more complex alternatives over short forecasting horizons.
These metrics provide a solid reference point for evaluating more advanced approaches such as log-return models, volatility-aware methods, or regime-switching frameworks
Model performance metrics are summarized and ranked by RMSE.
This comparison identifies:
The strongest baseline model
Whether increased model complexity improves accuracy
A benchmark for future, more advanced models
Such summaries are essential for transparent and reproducible model selection.
acc_summary <- acc %>%
select(.model, MAE, RMSE, MAPE) %>%
arrange(RMSE)
acc_summary
## # A tibble: 3 × 4
## .model MAE RMSE MAPE
## <chr> <dbl> <dbl> <dbl>
## 1 drift 1196. 1304. 1.36
## 2 arima 1276. 1371. 1.45
## 3 naive 1328. 1430. 1.51
The evaluation results compare the out-of-sample forecasting performance of the three baseline models over the test horizon.
Key observations from the results:
The Random Walk with Drift model achieves the best overall performance, with the lowest MAE, RMSE, and MAPE among the three models.
The ARIMA model performs moderately well but does not outperform the simpler drift model, indicating that additional model complexity does not translate into better short-term accuracy in this case.
The Naive model shows the weakest performance, serving as a useful lower-bound benchmark for forecast accuracy.
Metric-based interpretation:
Overall, these results suggest that for short-horizon Bitcoin price forecasting, simple baseline models—particularly Random Walk with Drift—are highly competitive, and more complex models must demonstrate clear gains to justify their added complexity.
This analysis successfully models Bitcoin daily prices as a structured time series.
Key outcomes:
Data validated and converted into a daily tsibble
Core price dynamics explored through EDA
Baseline forecasting models established and evaluated
A robust foundation created for advanced financial time series modeling
The analysis is now ready for extensions such as:
Log-return modeling
Volatility models (GARCH)
Weekly aggregation
Regime and structural break analysis