Enhance Bitcoin vs M2 chart with improved plotting and indexing

Refactor `bitcoin_m2_chart.py` to plot Bitcoin price on a log scale against a rebased M2 money supply index (base 100). Update visual elements for clarity, including color adjustments and axis labels. Extend x-axis for future projections and improve x-tick formatting for better readability. Add new output images for the updated chart.
This commit is contained in:
thePR0M3TH3AN committed 2025-05-31 21:38:31 -04:00
1 parent c9e2f285c3
commit e32d52583c
5 files changed
+59 -25

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+59 -25
View File
@@ -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,