diff --git a/outputs/bitcoin_m2_chart_20250531_201621.png b/outputs/bitcoin_m2_chart_20250531_201621.png new file mode 100644 index 0000000..9053d47 Binary files /dev/null and b/outputs/bitcoin_m2_chart_20250531_201621.png differ diff --git a/outputs/bitcoin_m2_chart_20250531_213204.png b/outputs/bitcoin_m2_chart_20250531_213204.png new file mode 100644 index 0000000..9053d47 Binary files /dev/null and b/outputs/bitcoin_m2_chart_20250531_213204.png differ diff --git a/outputs/bitcoin_m2_chart_20250531_213434.png b/outputs/bitcoin_m2_chart_20250531_213434.png new file mode 100644 index 0000000..757bbdd Binary files /dev/null and b/outputs/bitcoin_m2_chart_20250531_213434.png differ diff --git a/outputs/bitcoin_m2_chart_20250531_213628.png b/outputs/bitcoin_m2_chart_20250531_213628.png new file mode 100644 index 0000000..6899f09 Binary files /dev/null and b/outputs/bitcoin_m2_chart_20250531_213628.png differ diff --git a/scripts/bitcoin_m2_chart.py b/scripts/bitcoin_m2_chart.py index d667811..9f73bbd 100644 --- a/scripts/bitcoin_m2_chart.py +++ b/scripts/bitcoin_m2_chart.py @@ -1,15 +1,11 @@ #!/usr/bin/env python3 """bitcoin_m2_chart.py ---------------------- -Plot Bitcoin price versus global M2 money supply stored in `data/fred.db`. - -The script reads two FRED series from the local SQLite database and -plots them on dual axes. The M2 series can be shifted relative to the -Bitcoin series by a configurable number of days. +Plot Bitcoin price (log scale) versus global M2 money supply (as a 100-point index) +stored in `data/fred.db`. Bitcoin is orange; M2 is green; white background; monthly x-ticks. """ from __future__ import annotations - import argparse from datetime import datetime from pathlib import Path @@ -23,7 +19,7 @@ import matplotlib.pyplot as plt import pandas as pd from dateutil.relativedelta import relativedelta -# IMPORTANT: use the package‐qualified import +# IMPORTANT: use the package-qualified import from scripts.common import ( fetch_series_db, save_figure, @@ -65,37 +61,76 @@ def plot_bitcoin_m2( height: float = 6, dpi: int | None = None, ) -> plt.Figure: - """Plot Bitcoin price and shifted M2 on dual axes.""" - # Determine overlapping range + """ + Plot Bitcoin price (log scale) and a rebased (100-point) M2 index on dual axes: + • Bitcoin is orange, on a log-scaled left y-axis + • Global M2 is green, on a linear right y-axis + • White background, monthly x-ticks, with an “empty buffer” past the last date. + """ + # 1) Determine overlapping date range common_start = max(btc.index.min(), m2.index.min()) common_end = min(btc.index.max(), m2.index.max()) - btc_common = btc.loc[common_start:common_end] - m2_common = m2.loc[common_start:common_end] + btc_common = btc.loc[common_start:common_end].dropna(subset=["value"]) + m2_common = m2.loc[common_start:common_end].dropna(subset=["value"]) + # 2) Shift M2 by `offset_days` m2_shifted = m2_common.shift(offset_days) + # 3) Rebase M2 to a 100-point index + # (so that at the first available date, M2_index = 100) + m2_indexed = (m2_shifted["value"] / m2_shifted["value"].iloc[0]) * 100 + + # 4) Create figure + axes fig, ax1 = plt.subplots(figsize=(width, height), dpi=dpi) + # (White background is default, so no style override is needed.) + + # 5) Plot Bitcoin (orange) ON A LOGARITHMIC Y-AXIS ax1.plot( - btc_common.index, btc_common["value"], label="Bitcoin (LHS)", color="#1f77b4" + btc_common.index, + btc_common["value"], + label="Bitcoin (USD, log-scale)", + color="#f7931a", # Bitcoin-orange + linewidth=2.0, ) - ax1.set_ylabel("Price (USD)") + ax1.set_yscale("log") # ← LOG SCALE + ax1.set_ylabel("Bitcoin Price (USD, log)", color="#333333") + ax1.tick_params(axis="y", colors="#333333") ax1.grid(axis="y", linestyle="--", alpha=0.3) + # 6) Plot M2 index (green) on a LINEAR right‐hand axis ax2 = ax1.twinx() ax2.plot( - m2_shifted.index, m2_shifted["value"], label="Global M2 (RHS)", color="#ff7f0e" + m2_indexed.index, + m2_indexed, + label="Global M2 Index (base 100)", + color="#00aa00", # dark-green for contrast + linewidth=2.0, ) - ax2.set_ylabel("Global M2") + ax2.set_ylabel("Global M2 Index (linear)", color="#333333") + ax2.tick_params(axis="y", colors="#333333") + # (No grid on ax2 to keep it clean.) - ax1.set_xlim(common_start, common_end + relativedelta(years=extend_years)) - ax1.xaxis.set_major_locator(mdates.YearLocator(base=5)) - ax1.xaxis.set_minor_locator(mdates.YearLocator()) - ax1.xaxis.set_major_formatter(mdates.DateFormatter("%Y")) + # 7) Extend x‐axis to give “future blank space” + x_end_extended = common_end + relativedelta(years=extend_years) + ax1.set_xlim(common_start, x_end_extended) + # 8) Monthly x-ticks (e.g., “Aug, Sep, Oct…”) + ax1.xaxis.set_major_locator(mdates.MonthLocator(interval=1)) + ax1.xaxis.set_major_formatter(mdates.DateFormatter("%b")) + ax1.xaxis.set_minor_locator(mdates.MonthLocator(bymonthday=15)) + plt.setp(ax1.get_xticklabels(), rotation=0, ha="center") + + # 9) Combine legends from both axes lines1, labels1 = ax1.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() - ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper left", frameon=False) + ax1.legend( + lines1 + lines2, + labels1 + labels2, + loc="upper left", + frameon=False, + fontsize=10, + ) fig.tight_layout() return fig @@ -127,7 +162,7 @@ def main(argv: list[str] | None = None) -> plt.Figure: p.add_argument( "--offset", type=int, - default=94, + default=94, # M2 leads by 94 days help="shift M2 by this many days (positive = M2 leads)", ) p.add_argument( @@ -162,17 +197,16 @@ def main(argv: list[str] | None = None) -> plt.Figure: btc, m2 = fetch_series(start_dt, end_dt, args.btc_series, args.m2_series, args.db) - # ─── Drop any rows where the FRED tables had NULLs in "value" ────────────────── + # Drop rows with null “value” btc = btc.dropna(subset=["value"]) m2 = m2.dropna(subset=["value"]) - # ──────────────────────────────────────────────────────────────────────────────── validate_series(btc, m2, args.btc_series, args.m2_series) fig = plot_bitcoin_m2( btc, m2, - args.offset, - args.extend_years, + offset_days=args.offset, + extend_years=args.extend_years, width=args.width, height=args.height, dpi=args.dpi,