Bitcoin Returns and Volatility Analysis

Analyze Bitcoin historical returns, compute log returns, measure drawdowns, and detect volatility regimes.

This page shows an AI data analysis example. A workflow is a list of prompts (instructions) used to complete one analysis task step by step. We send the same prompt list to multiple AI models, then compare their notebook outputs and scores.

What this AI workflow does

This AI Data Analyst workflow loads 4-hour Bitcoin historical price data from a CSV URL and plots the closing price time series. It computes log returns, summarizes the return distribution with skewness and kurtosis, and visualizes the distribution. It measures drawdowns over time and uses 30-day rolling volatility to flag high- and low-volatility regimes.

Who this example is for

This is for analysts and researchers who want a reproducible notebook workflow for basic crypto return and risk diagnostics. It helps quantify tail behavior, drawdown risk, and volatility regime shifts from raw price data.

Expected analysis outcomes

These are the results the AI workflow is expected to generate.

  • BTC closing price line chart
  • Log return distribution plot with skewness and kurtosis
  • Maximum drawdown time series chart
  • 30-day rolling volatility chart with high/low regime bands

Tools and libraries used

Main Python packages and tooling used to run this AI data analysis task.

  • pandas
  • numpy
  • matplotlib
  • seaborn
  • scipy

Prompt sequence

This is the exact list of prompts used in this workflow. The same prompt sequence is sent to each model so outputs and scores can be compared fairly.

  1. 1load Bitcoin price data from https://raw.githubusercontent.com/pplonski/datasets-for-start/refs/heads/master/bitcoin-historical-data/btc_4h_data_2018_to_2025.csv and plot closing prices
  2. 2calculate log returns and plot distribution with kurtosis and skewness
  3. 3compute and plot maximum drawdown over time
  4. 4identify high and low volatility regimes using 30-day rolling volatility

Model Comparison

We compared several LLM models on the same analysis task. The table below shows their scores side by side. You can also open each model run to check the full conversation and notebook results.

Model SourceScoreTaskExecutionOutputReasoningReliabilityRun
gemma4:31b10/102/22/23/32/21/1Open gemma4:31b conversation
gpt-5.410/102/22/23/32/21/1Open gpt-5.4 conversation
gpt-oss:120b10/102/22/23/32/21/1Open gpt-oss:120b conversation
qwen3-coder-next9/102/22/23/31/21/1Open qwen3-coder-next conversation
glm-5.16/102/21/22/31/20/1Open glm-5.1 conversation
qwen3.5:397b6/101/22/22/31/20/1Open qwen3.5:397b conversation
How scoring works

Each run is graded on five dimensions:

  • Task Completion (0-2): how fully the workflow answered the requested analysis task and completed required steps.
  • Execution Correctness (0-2): whether generated code is correct, consistent, and likely runnable without major fixes.
  • Output Quality (0-3): how well tables, charts, and results match the expected analytical outcomes.
  • Reasoning Quality (0-2): clarity and correctness of interpretation, explanation, and conclusions.
  • Reliability (0-1): robustness of the run, including low hallucination risk and stable behavior.

The final score is the sum of these dimensions, so total score = 0-10. Higher values indicate more complete, correct, and reliable notebook results for this workflow.

AI Analysis with gemma4:31b

This run uses AI with a step-by-step list of prompts to perform the analysis task Bitcoin Returns and Volatility Analysis. Below, you can review the full conversation generated with gemma4:31b. After the conversation, we show detailed score cards for this run.

load Bitcoin price data from https://raw.githubusercontent.com/pplonski/datasets-for-start/refs/heads/master/bitcoin-historical-data/btc_4h_data_2018_to_2025.csv and plot closing prices
Thinking

The user wants to load Bitcoin price data from a specific URL and plot the closing prices.

According to the system instructions:

  1. "When a dataset is not yet loaded... first load and inspect only: show shape, columns, and head()."
  2. "Do not combine first-load data inspection with plotting/modeling in the same response."
  3. "Return at most one Python code block per response."

So, the first step is to load the data and inspect it. I cannot plot yet.

Plan:

  1. Load the CSV from the URL using pandas.
  2. Display shape, columns, and the first few rows.

I'll start by loading the dataset and inspecting its structure to make sure everything is correct before we move to plotting.

Load bitcoin data
import pandas as pd

url = "https://raw.githubusercontent.com/pplonski/datasets-for-start/refs/heads/master/bitcoin-historical-data/btc_4h_data_2018_to_2025.csv"
df = pd.read_csv(url)

# Inspect the data
print(f"Dataset shape: {df.shape}")
print("\nColumns:")
print(df.columns.tolist())
df.head()
Dataset shape: (17958, 12)

Columns:
['Open time', 'Open', 'High', 'Low', 'Close', 'Volume', 'Close time', 'Quote asset volume', 'Number of trades', 'Taker buy base asset volume', 'Taker buy quote asset volume', 'Ignore']
Open time Open High Low Close Volume Close time Quote asset volume Number of trades Taker buy base asset volume Taker buy quote asset volume Ignore
0 2018-01-01 00:00:00.000000 13715.65 13715.65 13155.38 13410.03 1676.204807 2018-01-01 03:59:59.999000 2.251607e+07 19438 739.518666 9.937537e+06 0
1 2018-01-01 04:00:00.000000 13434.98 13818.55 13322.15 13570.35 1302.214836 2018-01-01 07:59:59.999000 1.770254e+07 18061 588.516222 7.998796e+06 0
2 2018-01-01 08:00:00.000000 13569.98 13735.24 13001.13 13220.56 1319.755931 2018-01-01 11:59:59.999000 1.765121e+07 17200 582.668293 7.792109e+06 0
3 2018-01-01 12:00:00.000000 13220.56 13330.00 12750.00 13247.00 1831.933153 2018-01-01 15:59:59.999000 2.391259e+07 21357 830.314747 1.083304e+07 0
4 2018-01-01 16:00:00.000000 13247.00 13290.65 12940.00 13240.37 1092.337234 2018-01-01 19:59:59.999000 1.434887e+07 13178 557.453779 7.324320e+06 0
Thinking

The dataset is loaded. Shape: (17958, 12). Columns: ['Open time', 'Open', 'High', 'Low', 'Close', 'Volume', 'Close time', 'Quote asset volume', 'Number of trades', 'Taker buy base asset volume', 'Taker buy quote asset volume', 'Ignore']. The target column for plotting is Close and the time axis is Open time. The Open time column is currently treated as a string (object). To plot a time series properly, it should be converted to datetime.

Goal: Plot closing prices. Current state: Data loaded and inspected. Missing: The plot itself.

Path B: Convert Open time to datetime and plot the Close column.

The data is loaded with 17,958 rows. The Close column contains the prices, and Open time provides the timestamps. I will now convert the timestamps to datetime objects and plot the closing prices.

Plot Bitcoin closing prices
import matplotlib.pyplot as plt
import seaborn as sns

# Convert Open time to datetime
df['Open time'] = pd.to_datetime(df['Open time'])

plt.figure(figsize=(12, 6))
sns.lineplot(data=df, x='Open time', y='Close')
plt.title('Bitcoin Closing Prices (2018-2025)')
plt.xlabel('Date')
plt.ylabel('Price (USD)')
plt.grid(True)
plt.show()
Thinking

The user wants to load Bitcoin price data and plot closing prices. The notebook state shows:

  1. Data was loaded from the URL.
  2. df.head() showed columns including Open time and Close.
  3. Open time was converted to datetime.
  4. A line plot of Close prices over Open time was generated.

The latest output is a Figure object. According to the sufficiency heuristics: "If the latest output is chart-only/visual-only and does not include underlying numeric/text values, choose Path B first to print the values used in the chart."

Wait, the original user goal was "load Bitcoin price data... and plot closing prices". The user didn't ask for summary statistics or specific values, just to plot them. However, the system instruction says: "If the latest output is chart-only/visual-only and does not include underlying numeric/text values, choose Path B first to print the values used in the chart."

Actually, the goal was simply to plot closing prices. The plot is there. But the heuristic suggests that if I only have a figure and not the values supporting the conclusion, I should provide the values. In this case, the "conclusion" is the visual trend of the price.

Let's look at the plot. It shows the trend. But to be thorough and follow the heuristic, I should probably show a summary of the closing prices (min, max, mean, or just the head/tail) to ground the visual in numbers.

Actually, the goal is "load... and plot". Both are done. But the heuristic says: "If the latest output is chart-only/visual-only and does not include underlying numeric/text values, choose Path B first to print the values used in the chart."

The latest output (output-2) is just <Figure size 1200x600 with 1 Axes>. It doesn't show the numeric values of the closing prices for the period. output-1 showed the first 5 rows, but not the full range.

So I will choose Path B to show a summary of the Close prices (like min, max, and current price) to provide concrete context to the plot.

Wait, the user prompt asks me to analyze the latest executed notebook outputs and notebook state against the original user goal.

Goal: Load data and plot closing prices. Status: Data loaded, plot generated. Heuristic: Path B if chart-only.

I'll provide a brief insight about the trend seen in the plot (which is implied by the data range 2018-2025) and print the summary statistics of the Close column to satisfy the "numeric/text values" requirement.

Insights from outputs:

  • The dataset spans from 2018 to 2025.
  • The plot visualizes the volatility and growth of Bitcoin closing prices over this period.
  • The initial prices around January 2018 were approximately 13,000 USD.

Now, I need one runnable Python code block to show the underlying numeric summary.

df['Close'].describe()

Wait, the instructions say: "If Path B is chosen: Provide exactly one runnable Python code block... Code must be minimal and materially different... For chart-only outputs, the code should print/display the underlying values (for example a compact top-N table) before finalizing."

I will print the summary statistics of the closing prices.

The plot visualizes the price evolution of Bitcoin from 2018 to 2025, showing significant volatility and long-term growth. While the visualization captures the trend, the starting prices in early 2018 were around 13,000 USD. To provide precise context to the visual trend, I will now extract the summary statistics of the closing prices.

Summarize Close prices
df['Close'].describe()
count     17958.000000
mean      38271.263441
std       32485.932221
min        3172.050000
25%        9552.432500
50%       28166.940000
75%       59472.957500
max      125410.810000
Name: Close, dtype: float64
Thinking

The user goal was to load Bitcoin price data from a specific URL and plot the closing prices.

Reviewing the notebook state:

  1. Cell 1: Loaded the data from the URL. df.shape is (17958, 12). Columns include 'Open time' and 'Close'.
  2. Cell 2: Converted 'Open time' to datetime and plotted 'Close' prices using a line plot.
  3. Cell 3: Ran df['Close'].describe(), which provided summary statistics: min 3172.05 USD, max 125410.81 USD, and mean 38271.26 USD.

The original goal was "load Bitcoin price data ... and plot closing prices".

  • Data loaded? Yes.
  • Plotting closing prices? Yes (Cell 2).
  • Supporting numeric values? Yes (Cell 3 provide stats).

The sufficiency heuristic says: "If the goal is to show/plot/chart/visualize and latest output contains both a valid chart/figure object and explicit numeric/text values supporting the conclusion, choose Path A".

Insights:

  • The dataset contains 17,958 records of 4-hour interval Bitcoin data.
  • The closing prices exhibit extreme volatility, ranging from a minimum of 3,172.05 USD to a maximum of 125,410.81 USD.
  • The visualization shows a clear upward trajectory over the 2018-2025 period despite the volatility.

Conclusion: The original goal has been fully met.

The Bitcoin historical data has been successfully loaded and visualized. The closing prices exhibit significant volatility over the 2018-2025 period, with a minimum value of 3,172.05 USD and a peak of 125,410.81 USD. The plot confirms a long-term upward trend, while the summary statistics provide a quantitative scale to the price swings observed in the visualization.

calculate log returns and plot distribution with kurtosis and skewness
Thinking

The user wants to calculate log returns for the Bitcoin price data, plot the distribution of these returns, and determine the kurtosis and skewness of the distribution.

*   `df` contains the data.
*   `df['Close']` is the closing price.
*   `df['Open time']` is already converted to datetime.

*   Formula for log returns: $r_t = \ln(P_t / P_{t-1}) = \ln(P_t) - \ln(P_{t-1})$.
*   In pandas: `np.log(df['Close'] / df['Close'].shift(1))` or `np.log(df['Close']).diff()`.
*   Need `numpy` for the log function.
*   Need `scipy.stats` or pandas' built-in `.skew()` and `.kurt()` for statistics.
*   Distribution plot: `sns.histplot` or `sns.kdeplot`.

*   Step 1: Calculate log returns.
*   Step 2: Calculate skewness and kurtosis.
*   Step 3: Plot the distribution (histogram + KDE).

*   *Action:* Calculate log returns using `numpy`.
*   *Action:* Compute skewness and kurtosis using pandas methods.
*   *Action:* Plot using seaborn.

*Wait*, the system instruction says: "Prefer one execution step at a time." and "Do not provide a full end-to-end pipeline at once unless user explicitly asks." and "Default to one chart OR one table per step."

The user asked for calculations AND a plot AND stats. I should break this down.
1. Calculate log returns and show the stats (skew/kurt).
2. Plot the distribution.

Actually, calculating the returns is a prerequisite for the plot and the stats. I can calculate the returns and output the stats in one go, then plot in the next.

Wait, "one runnable Python code block per response".

Let's do the calculation and the summary stats (skew/kurt) first. Then the plot.

*Correction:* The user wants "calculate log returns and plot distribution with kurtosis and skewness". I can do the calculation and the plot in one go if the stats are just printed or added to the title/legend. But to be safe and follow the "one chart/table per step" rule, I will first calculate the returns and the stats.

Actually, calculating the returns is just a transformation. I'll calculate the returns and the statistics first.

Wait, I see the request is a single multi-part goal. I'll combine the calculation and the stats (table/text) first.

Actually, it's better to do the calculation and the stats, and then in the next turn, do the visualization.

Step 1: Calculate log returns and compute skewness/kurtosis.

Wait, I should probably just do the calculations and the plot together if it's a single "analysis" step, but the instructions are strict: "Default to one chart OR one table per step."

Let's calculate the log returns and compute the stats first.

*Drafting code:*
```python
import numpy as np
df['Log_Returns'] = np.log(df['Close'] / df['Close'].shift(1))
skew = df['Log_Returns'].skew()
kurt = df['Log_Returns'].kurt()
# return these
```

Actually, I can calculate the returns and show the descriptive statistics (which include skew/kurt if I calculate them manually).

Let's refine:
1. Import numpy.
2. Compute log returns.
3. Calculate skewness and kurtosis.
4. Print/display these values.

Next step will be the plot.

*Wait*, usually, users want to see the plot immediately with the stats. I'll provide the code to calculate and compute the stats first.

*Rethink:* If I provide the code to calculate and compute stats, and then the user has to run it, then I can plot.

Let's check if I can do it in one go. "Return exactly one runnable Python code block per response".

Okay, I'll calculate the log returns and the stats first.

Actually, let's just do the calculation and the statistics.

*Code:*
```python
import numpy as np

df['Log_Returns'] = np.log(df['Close'] / df['Close'].shift(1))
skewness = df['Log_Returns'].skew()
kurtosis = df['Log_Returns'].kurt()

print(f"Skewness: {skewness:.4f}")
print(f"Kurtosis: {kurtosis:.4f}")
df['Log_Returns'].describe()
```

Wait, I should explain it simply.

"I will first calculate the log returns and determine the skewness and kurtosis to understand the distribution's shape."

Wait, looking at the user's prompt: "calculate log returns and plot distribution with kurtosis and skewness".
The "with kurtosis and skewness" might imply putting them on the plot.

If I follow the "one chart OR one table" rule, I'll do the calculation and stats first.

Let's go.

I will first calculate the log returns and compute the skewness and kurtosis. This allows us to quantify the asymmetry and the "fatness" of the tails of the return distribution before visualizing them.

Compute return statistics
import numpy as np

# Calculate log returns: ln(Pt / Pt-1)
df['Log_Returns'] = np.log(df['Close'] / df['Close'].shift(1))

# Compute skewness and kurtosis
skewness = df['Log_Returns'].skew()
kurtosis = df['Log_Returns'].kurt()

print(f"Skewness: {skewness:.4f}")
print(f"Kurtosis: {kurtosis:.4f}")
df['Log_Returns'].describe()
Skewness: -0.4951
Kurtosis: 15.6377
count    17957.000000
mean         0.000093
std          0.013950
min         -0.229366
25%         -0.004723
50%          0.000194
75%          0.005121
max          0.137577
Name: Log_Returns, dtype: float64
Thinking

The user wants to calculate log returns and plot the distribution, including kurtosis and skewness. The previous step calculated the log returns, skewness (-0.4951), and kurtosis (15.6377). However, the "plot distribution" part of the request has not been executed yet. The last output was numeric descriptions and metrics.

Analysis of outputs:

  • Skewness: -0.4951 (Negative skew indicates a longer tail on the left, meaning more frequent small gains and occasional large losses).
  • Kurtosis: 15.6377 (High kurtosis, much greater than 3, indicates a "leptokurtic" distribution with fat tails and a sharp peak, meaning extreme price movements are more common than in a normal distribution).

Missing part: The plot of the distribution.

Therefore, I must choose Path B to generate the plot.

Plan:

  1. Provide insights on the skewness and kurtosis values.
  2. Provide a Python code block to plot the distribution of Log_Returns (histogram and KDE).

The calculated skewness of -0.4951 indicates a distribution with a longer tail on the left, suggesting that large negative returns occur more frequently than large positive ones. The kurtosis of 15.6377 is significantly higher than that of a normal distribution (which is 3), confirming that Bitcoin returns have "fat tails" and are prone to extreme price swings.

To complete the original goal, I will now plot the distribution of these log returns.

Plot log returns distribution
import matplotlib.pyplot as plt
import seaborn as sns

plt.figure(figsize=(10, 6))
sns.histplot(df['Log_Returns'].dropna(), kde=True, bins=100, color='blue')
plt.title(f'Distribution of Bitcoin Log Returns\nSkewness: {skewness:.4f}, Kurtosis: {kurtosis:.4f}')
plt.xlabel('Log Return')
plt.ylabel('Frequency')
plt.grid(True, alpha=0.3)
plt.show()
Thinking

The user wants to calculate log returns and plot the distribution with kurtosis and skewness.

Looking at the notebook state:

  • Cell 4: Calculated Log_Returns, computed skewness (-0.4951) and kurtosis (15.6377), and showed the describe() summary.
  • Cell 5: Plotted the distribution using a histogram and KDE, including the skewness and kurtosis values in the title.

The outputs provide:

  1. Numeric values for skewness and kurtosis.
  2. A statistical summary of the log returns.
  3. A visual representation of the distribution.

All parts of the original goal ("calculate log returns and plot distribution with kurtosis and skewness") have been fulfilled.

The skewness of -0.4951 indicates a left-skewed distribution, meaning there is a higher frequency of extreme negative returns compared to extreme positive ones. The kurtosis of 15.6377 is very high (leptokurtic), indicating that the distribution has fat tails and a sharp peak, which is characteristic of financial assets where "black swan" events (extreme price movements) occur more often than a normal distribution would predict.

Since all requirements are met and both numeric evidence and visualization are present, Path A is appropriate.

Final check on currency: No currency values are needed in the final answer, but I will be careful if I mention the closing prices (which are in USD). However, the focus is on returns (percentages/logs), not absolute prices.

Final Answer structure:

  • Insights on skewness.
  • Insights on kurtosis.
  • Connection to the goal.
  • Mark as <!--final answer-->.

The log returns have been calculated and visualized, providing a clear picture of Bitcoin's price volatility. The skewness of -0.4951 indicates a left-skewed distribution, meaning the asset experiences extreme negative price swings more frequently than extreme positive ones. The kurtosis of 15.6377 is significantly higher than 3 (the kurtosis of a normal distribution), confirming that the returns are leptokurtic with "fat tails." This suggests that extreme market events—both crashes and surges—occur more often than would be expected in a standard bell curve.

compute and plot maximum drawdown over time
Thinking

The user wants to compute and plot the "Maximum Drawdown" (MDD) over time for Bitcoin based on the existing notebook state.

*   `df`: DataFrame containing Bitcoin historical data.
*   Columns include `Open time` (datetime) and `Close` (price).
*   `Log_Returns` are already calculated.

*   Drawdown is the peak-to-trough decline during a specific period for an investment.
*   Formula: $Drawdown = \frac{\text{Current Value} - \text{Peak Value}}{\text{Peak Value}}$
*   Maximum Drawdown is the maximum loss from a peak.
*   To plot it over time, I need a running maximum of the closing price.

*   Calculate the running peak: `cummax = df['Close'].cummax()`.
*   Calculate the drawdown: `drawdown = (df['Close'] - cummax) / cummax`.
*   Plot the `drawdown` series over `Open time`.

*   Import necessary libraries (already done in previous cells, but good to keep code self-contained if needed, though I should follow the iterative rule).
*   One code block for calculation and plotting.

To compute the maximum drawdown, we need to track the historical peak of the closing price and calculate the percentage drop from that peak at every point in time.

I will calculate the running maximum of the closing price, compute the drawdown series, and plot it to show how the investment value declined from its peaks over time.

Calculate and plot drawdown
# Calculate the running maximum price
df['Peak'] = df['Close'].cummax()

# Calculate drawdown as a percentage
df['Drawdown'] = (df['Close'] - df['Peak']) / df['Peak']

# Find the Maximum Drawdown (MDD)
mdd = df['Drawdown'].min()

plt.figure(figsize=(12, 6))
plt.fill_between(df['Open time'], df['Drawdown'], 0, color='red', alpha=0.3)
plt.plot(df['Open time'], df['Drawdown'], color='red', linewidth=1)
plt.title(f'Bitcoin Drawdown Over Time\nMaximum Drawdown: {mdd:.2%}')
plt.xlabel('Date')
plt.ylabel('Drawdown (%)')
plt.grid(True, alpha=0.3)
plt.show()
Thinking

The user wants to compute and plot the maximum drawdown over time. Looking at the notebook state:

  • Cell 6 calculates the running maximum (Peak), the Drawdown series, and the mdd (Maximum Drawdown).
  • Cell 6 plots the drawdown over time with a title that includes the mdd formatted as a percentage.
  • The output of Cell 6 is a figure.

The user goal was "compute and plot maximum drawdown over time". The code in Cell 6 did both:

  1. Computed df['Drawdown'] and mdd.
  2. Plotted the result.

However, the "Sufficiency heuristics" state: "If the latest output is chart-only/visual-only and does not include underlying numeric/text values, choose Path B first to print the values used in the chart." In Cell 6, mdd is used in the title of the plot, but the numeric value of mdd is not printed to the notebook output as a standalone text/variable (it's inside the figure title, and the figure object is shown as <Figure size 1200x600 with 1 Axes>). While the title technically contains the value, usually "printing the value" means a text output in the notebook.

Wait, the prompt says "If the goal is to show/plot/chart/visualize and latest output contains both a valid chart/figure object and explicit numeric/text values supporting the conclusion, choose Path A". The title of the plot f'Bitcoin Drawdown Over Time\nMaximum Drawdown: {mdd:.2%}' contains the value. But the output shown in the provided notebook state for Cell 6 is just <Figure size 1200x600 with 1 Axes>. The actual text of the title is not "printed" in the output log provided to me, although the code executed it.

Actually, let's look at the provided notebook output for Cell 6: [output-6] <Figure size 1200x600 with 1 Axes>. The numeric value of mdd is not explicitly printed as a return value or a print statement in the output block. It's only in the plot title.

According to the rule: "If the latest output is chart-only/visual-only and does not include underlying numeric/text values, choose Path B first to print the values used in the chart."

Therefore, I should choose Path B to print the actual numeric value of the Maximum Drawdown (MDD) to make it explicit in the notebook logs, complementing the plot.

Wait, let's re-read. The user goal is "compute and plot maximum drawdown over time". The computation is done. The plot is done. If I follow the heuristic strictly, I need to print the numeric value.

Let's check if I can just provide the final answer. The user goal was to compute and plot. Both are achieved. But the heuristic is very specific about "chart-only" outputs.

Let's execute Path B to print the MDD value.

Wait, the MDD is calculated as mdd = df['Drawdown'].min(). I will print the MDD and perhaps the date it occurred.

Code:

print(f"Maximum Drawdown: {mdd:.2%}")

But mdd is a local variable in Cell 6. It should be available in the notebook state. Actually, looking at the notebook state [variables], mdd is NOT listed in the variables list. Only df is listed. If mdd is not in the variables list, I cannot print it directly without recalculating or referencing the dataframe. The variables list provided: count: 11 | truncated: false items: - df:DataFrame ... It seems only df was captured.

So I need to calculate it again or access it via df['Drawdown'].min().

Let's double check the goal. "compute and plot maximum drawdown over time". The plot is there. The computation is there. If I just provide a final answer, I'm ignoring the "chart-only" heuristic. If I do Path B, I provide the numeric value.

Let's provide the numeric value.

One more check on the "A/B decision policy": "If the latest output is chart-only/visual-only and does not include underlying numeric/text values, choose Path B first to print the values used in the chart." Output 6 is <Figure size 1200x600 with 1 Axes>. This is definitely chart-only.

Path B it is.

What values are most relevant?

  1. The Maximum Drawdown percentage.
  2. The date of the peak and the date of the trough (the specific period of the max drawdown).

The prompt says "minimal runnable Python code block to gather the missing evidence". Printing the MDD value is the most direct missing piece.

mdd_value = df['Drawdown'].min()
print(f"Maximum Drawdown: {mdd_value:.2%}")

Actually, I can provide more insight. The maximum drawdown represents the largest peak-to-trough decline.

Let's refine the insight. The drawdown plot shows the percentage loss from the historical peak. The "fat tails" observed in the returns distribution (high kurtosis) are reflected here as significant, deep drawdowns.

Plan:

  1. Identify that the plot was generated but the specific MDD value isn't explicitly printed in the notebook output (only in the plot title).
  2. Path B: Print the MDD value.

Wait, looking at the provided [variables], mdd is not there. So I must calculate it from df.

print(f"Maximum Drawdown: {df['Drawdown'].min():.2%}")

Is there anything else? The user asked to "compute and plot". I'll stick to the minimal print.

The drawdown plot illustrates the percentage decline from the historical peak price. The high kurtosis observed in the log returns is reflected here as deep, significant drawdowns, indicating that Bitcoin is subject to severe periodic crashes. While the plot visualizes these trends, the exact numeric value of the maximum drawdown is not explicitly listed in the notebook's text outputs.

Print maximum drawdown
# Print the numeric value of the Maximum Drawdown for explicit record
mdd_value = df['Drawdown'].min()
print(f"Maximum Drawdown: {mdd_value:.2%}")
Maximum Drawdown: -81.42%
Thinking

The user wants to compute and plot the maximum drawdown over time.

Looking at the notebook state:

  • Cell 6 calculated the Peak and Drawdown columns.
  • Cell 6 plotted the drawdown over time and displayed the MDD in the title.
  • Cell 7 explicitly printed the Maximum Drawdown value: -81.42%.

The original goal was to "compute and plot maximum drawdown over time".

  1. Compute: Done (df['Drawdown'] and mdd_value).
  2. Plot: Done (Cell 6).

The outputs provide both the visual representation (plot) and the numeric value (-81.42%).

According to the sufficiency heuristics: "If the goal is to show/plot/chart/visualize and latest output contains both a valid chart/figure object and explicit numeric/text values supporting the conclusion, choose Path A".

Conclusion: The goal is achieved.

Final Answer Content:

  • Identify the maximum drawdown value from the output.
  • Explain the visual result (the red area represents the loss from the previous peak).
  • Connect the numeric value (-81.42%) to the result.

The maximum drawdown for Bitcoin over the period from 2018 to 2025 was -81.42%.

The plot illustrates that Bitcoin experienced several significant crashes where the price dropped sharply from its previous all-time highs. The red shaded area represents the percentage loss from the historical peak at any given point in time, highlighting the high volatility and the depth of the most severe correction.

identify high and low volatility regimes using 30-day rolling volatility
Thinking
  • Current state: Bitcoin historical data (2018-2025) loaded in df. Log returns are already calculated in df['Log_Returns'].
    • User goal: Identify high and low volatility regimes using a 30-day rolling volatility.

    • Specifics:

      • Rolling window: 30 days.
      • Data frequency: 4-hour intervals (from the URL btc_4h_data...).
      • Need to calculate rolling standard deviation of returns.
      • Since it's 4h data, 30 days = $30 \times (24/4) = 30 \times 6 = 180$ periods.
    • Calculate the rolling standard deviation of Log_Returns.

    • The window size needs to be adjusted for 4h frequency (180 periods).

    • Plot the rolling volatility to visually identify regimes.

    • (Optional/Later) Define a threshold for "high" vs "low" volatility, but the prompt just says "identify". Usually, plotting it is the first step to seeing the regimes.

    • Step 1: Calculate 30-day rolling volatility and plot it.

    • Window = $30 \times 6 = 180$.

    • df['Rolling_Vol'] = df['Log_Returns'].rolling(window=180).std()

    • Plot using sns.lineplot.

To identify volatility regimes, we first need to calculate the rolling standard deviation of the log returns. Since the data is sampled every 4 hours, a 30-day window corresponds to 180 periods (`30 \text{ days} × 6 \text{ periods/day}`).

I will calculate this rolling volatility and plot it over time to visualize the high and low regime shifts.

Plot rolling volatility
# 30 days * 6 periods per day (4h data) = 180 periods
window_size = 30 * 6
df['Rolling_Vol'] = df['Log_Returns'].rolling(window=window_size).std()

plt.figure(figsize=(12, 6))
sns.lineplot(data=df, x='Open time', y='Rolling_Vol', color='purple')
plt.title('Bitcoin 30-Day Rolling Volatility (4h Intervals)')
plt.xlabel('Date')
plt.ylabel('Volatility (Std Dev of Log Returns)')
plt.grid(True, alpha=0.3)
plt.show()
Thinking

The user wants to "identify high and low volatility regimes using 30-day rolling volatility". Looking at the notebook state:

  • Cell 8 calculated the 30-day rolling volatility (Rolling_Vol) and plotted it.
  • The output of cell 8 is a figure.

The goal is to identify high and low volatility regimes. A chart shows them visually, but it doesn't quantitatively "identify" them (e.g., by defining thresholds or labeling periods as high/low). The user's prompt asks to identify these regimes. Simply plotting the line is a start, but not a definitive identification of "regimes".

According to the sufficiency heuristics: "If the latest output is chart-only/visual-only and does not include underlying numeric/text values, choose Path B first to print the values used in the chart."

To identify regimes, I need to define what constitutes "high" and "low" volatility. A common way is to use quantiles (e.g., top 25% as high, bottom 25% as low) or a specific threshold based on the mean/std of the volatility series.

Current state:

  • df['Rolling_Vol'] exists.
  • We have a plot.
  • We don't have numeric thresholds or a classification of timestamps into regimes.

Path B is required. I need to calculate the thresholds (e.g., 25th and 75th percentiles) to objectively identify the high and low volatility regimes and perhaps list some examples or a summary table.

Plan:

  1. Calculate the 25th and 75th percentiles of Rolling_Vol.
  2. Categorize the periods into 'Low', 'Normal', and 'High'.
  3. Display the thresholds and a summary of how many periods fall into each regime.

Refined Path B:

  • Compute percentiles for Rolling_Vol.
  • Create a 'Regime' column.
  • Print the threshold values and the count of periods in each regime.

The rolling volatility plot shows clear fluctuations, with several distinct spikes representing high-volatility periods and prolonged troughs representing low-volatility regimes. To objectively identify these regimes, we need to establish numeric thresholds based on the distribution of the rolling volatility.

I will use the 25th and 75th percentiles to define "Low" and "High" volatility regimes, providing the exact threshold values and the proportion of time Bitcoin spends in each state.

Categorize volatility regimes
# Define thresholds based on quantiles
low_threshold = df['Rolling_Vol'].quantile(0.25)
high_threshold = df['Rolling_Vol'].quantile(0.75)

# Categorize regimes
df['Vol_Regime'] = pd.cut(
    df['Rolling_Vol'], 
    bins=[-float('inf'), low_threshold, high_threshold, float('inf')], 
    labels=['Low', 'Normal', 'High']
)

# Summary of regimes
regime_counts = df['Vol_Regime'].value_counts(normalize=True) * 100

print(f"Low Volatility Threshold: {low_threshold:.6f}")
print(f"High Volatility Threshold: {high_threshold:.6f}")
print("\nPercentage of time in each regime:")
regime_counts
Low Volatility Threshold: 0.009177
High Volatility Threshold: 0.014724

Percentage of time in each regime:
Vol_Regime
Normal    49.994375
Low       25.002812
High      25.002812
Name: proportion, dtype: float64
Thinking

The user wants to identify high and low volatility regimes using 30-day rolling volatility.

*   `df['Rolling_Vol']` was calculated using a window of 180 periods (30 days * 6 periods/day).
*   A plot was generated showing volatility over time.
*   Quantiles (25th and 75th) were used to define thresholds.
*   `Low Volatility Threshold`: 0.009177
*   `High Volatility Threshold`: 0.014724
*   `Vol_Regime` column was created with 'Low', 'Normal', and 'High'.
*   Distribution: ~25% Low, ~50% Normal, ~25% High.

The original goal was to "identify high and low volatility regimes using 30-day rolling volatility". The analyst has:

  1. Calculated the metric (30-day rolling vol).
  2. Visualized it.
  3. Quantitatively defined the regimes using statistical thresholds.
  4. Categorized the data into these regimes.
  5. Summarized the proportion of time spent in each regime.

Is this sufficient? Yes. The "identification" consists of the logic used (quantiles), the specific thresholds found, and the resulting classification of the timeline.

Path A is appropriate.

Concrete insights:

  • The thresholds for volatility are defined at $0.009177$ for low volatility and $0.014724$ for high volatility.
  • Bitcoin spends approximately 25% of its time in a low volatility regime and 25% in a high volatility regime, with the remaining 50% characterized as normal volatility.
  • These regimes allow for the objective categorization of market states based on the 30-day rolling standard deviation of log returns.

Wait, let me double check if a visualization of the regimes on the price chart is needed for "identifying" them? The goal says "identify", which usually means defining them. The thresholds and the counts are the identification. However, as a data analyst, providing the conclusion is the end goal.

Check formatting:

  • No $ for money. (No money mentioned in these specific outputs, but I'll be careful).
  • No "Step" headers.
  • No "Final Answer" phrases unless <!--final answer--> is used.
  • No chain of thought.

Final Answer Structure:

  • State the identified thresholds.
  • State the distribution of regimes.
  • Conclude that these define the high and low regimes.

The high and low volatility regimes for Bitcoin have been identified using the 25th and 75th percentiles of the 30-day rolling standard deviation of log returns.

The identified thresholds are:

  • Low Volatility Regime: Volatility ≤ 0.009177
  • High Volatility Regime: Volatility ≥ 0.014724
  • Normal Volatility Regime: Values between $0.009177$ and $0.014724$

Based on these thresholds, Bitcoin spends approximately 25% of its time in a low volatility state and 25% in a high volatility state, while the remaining 50% of the period is characterized by normal volatility. This quantitative approach objectively segments the timeline into distinct risk regimes.

Score cards for gemma4:31b for Bitcoin Returns and Volatility Analysis

Total score: 10/10

  • Task Completion: 2/2

    Excellent

    All major requested steps were completed: price plot, log-return distribution with skewness/kurtosis, drawdown analysis, and rolling volatility regime identification.

  • Execution Correctness: 2/2

    Excellent

    The code shown is coherent and likely runnable; it uses standard pandas/numpy/matplotlib/seaborn operations without obvious syntax or logic errors.

  • Output Quality: 3/3

    Excellent

    Outputs match the expected outcomes semantically, including the BTC price chart, fat-tailed return distribution with kurtosis > 3, drawdown chart, and rolling volatility/regime summary.

  • Reasoning Quality: 2/2

    Excellent

    The workflow correctly explains each analytical step and ties the computations to the stated goals, with appropriate interpretation of the results.

  • Reliability: 1/1

    Excellent

    The workflow is consistent and uses reasonable methods for the analysis, with no major hallucinations or unsafe behavior evident.

Try MLJAR Studio

Run the same type of AI-powered data analysis on your own datasets with conversational notebooks in MLJAR Studio.

Explore More AI Analysis Examples

Discover additional workflows across categories. Each example includes prompts, conversation outputs, and model-level scoring so you can compare approaches and results.