mirror of
https://github.com/PR0M3TH3AN/python_charts.git
synced 2026-10-27 08:50:39 +00:00
Add Bitcoin vs Global M2 chart
This commit is contained in:
1 parent
8ce343b5a2
commit
8c114bab87
3 files changed
+216
No files matched your search
@@ -119,6 +119,14 @@ The same invocation is available via Make:
|
||||
```bash
|
||||
make plot-custom ARGS="--series UNRATE DCOILWTICO --start 2000-01-01 --end 2020-12-31"
|
||||
```
|
||||
### Bitcoin vs. Global M2
|
||||
|
||||
After downloading the required series into `data/fred.db`, generate the Bitcoin overlay chart:
|
||||
|
||||
```bash
|
||||
./startup.sh python scripts/bitcoin_m2_chart.py --btc-series CBBTCUSD --m2-series GLOBAL_M2
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import matplotlib.dates as mdates
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import sqlite3
|
||||
|
||||
|
||||
def fetch_series(
|
||||
start: datetime,
|
||||
end: datetime,
|
||||
btc_series: str,
|
||||
m2_series: str,
|
||||
db_path: str | Path = Path("data/fred.db"),
|
||||
) -> Tuple[pd.DataFrame, pd.DataFrame]:
|
||||
"""Load Bitcoin and M2 series from a local SQLite DB."""
|
||||
db_path = Path(db_path)
|
||||
if not db_path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"{db_path} not found. Run scripts/refresh_data.py on a machine with internet, commit the DB, then retry."
|
||||
)
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
btc = pd.read_sql(
|
||||
f"SELECT date, {btc_series} AS value FROM {btc_series}",
|
||||
conn,
|
||||
parse_dates=["date"],
|
||||
index_col="date",
|
||||
).loc[start:end]
|
||||
m2 = pd.read_sql(
|
||||
f"SELECT date, {m2_series} AS value FROM {m2_series}",
|
||||
conn,
|
||||
parse_dates=["date"],
|
||||
index_col="date",
|
||||
).loc[start:end]
|
||||
return btc, m2
|
||||
|
||||
|
||||
def plot_bitcoin_m2(
|
||||
btc: pd.DataFrame,
|
||||
m2: pd.DataFrame,
|
||||
offset_days: int,
|
||||
extend_years: int,
|
||||
) -> plt.Figure:
|
||||
"""Plot Bitcoin price and shifted M2 on dual axes."""
|
||||
# Determine overlapping 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]
|
||||
|
||||
m2_shifted = m2_common.shift(offset_days)
|
||||
|
||||
fig, ax1 = plt.subplots(figsize=(12, 6))
|
||||
ax1.plot(
|
||||
btc_common.index, btc_common["value"], label="Bitcoin (LHS)", color="#1f77b4"
|
||||
)
|
||||
ax1.set_ylabel("Price (USD)")
|
||||
ax1.grid(axis="y", linestyle="--", alpha=0.3)
|
||||
|
||||
ax2 = ax1.twinx()
|
||||
ax2.plot(
|
||||
m2_shifted.index, m2_shifted["value"], label="Global M2 (RHS)", color="#ff7f0e"
|
||||
)
|
||||
ax2.set_ylabel("Global M2")
|
||||
|
||||
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"))
|
||||
|
||||
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)
|
||||
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> plt.Figure:
|
||||
p = argparse.ArgumentParser(description="Plot Bitcoin vs Global M2")
|
||||
p.add_argument(
|
||||
"--btc-series",
|
||||
type=str,
|
||||
default="CBBTCUSD",
|
||||
help="FRED series name for Bitcoin price",
|
||||
)
|
||||
p.add_argument(
|
||||
"--m2-series",
|
||||
type=str,
|
||||
default="GLOBAL_M2",
|
||||
help="FRED series name for global M2",
|
||||
)
|
||||
p.add_argument(
|
||||
"--start", type=str, default="2010-01-01", help="start date YYYY-MM-DD"
|
||||
)
|
||||
p.add_argument(
|
||||
"--end",
|
||||
type=str,
|
||||
default=datetime.today().strftime("%Y-%m-%d"),
|
||||
help="end date YYYY-MM-DD",
|
||||
)
|
||||
p.add_argument(
|
||||
"--offset-days",
|
||||
type=int,
|
||||
default=94,
|
||||
help="shift M2 by this many days (positive = M2 leads)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--extend-years", type=int, default=1, help="years beyond end date to show"
|
||||
)
|
||||
p.add_argument(
|
||||
"--db", type=str, default="data/fred.db", help="path to local SQLite DB"
|
||||
)
|
||||
p.add_argument(
|
||||
"--output", type=str, default=None, help="optional path to save the figure"
|
||||
)
|
||||
args = p.parse_args(argv)
|
||||
|
||||
start_dt = datetime.fromisoformat(args.start)
|
||||
end_dt = datetime.fromisoformat(args.end)
|
||||
|
||||
btc, m2 = fetch_series(start_dt, end_dt, args.btc_series, args.m2_series, args.db)
|
||||
fig = plot_bitcoin_m2(btc, m2, args.offset_days, args.extend_years)
|
||||
|
||||
if args.output:
|
||||
fig.savefig(args.output)
|
||||
if argv is None:
|
||||
fig.show()
|
||||
return fig
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,57 @@
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pandas as pd
|
||||
|
||||
sys.path.append(os.path.abspath("."))
|
||||
|
||||
from scripts.bitcoin_m2_chart import plot_bitcoin_m2, main as btc_m2_main
|
||||
|
||||
|
||||
def sample_data():
|
||||
dates = pd.date_range("2021-01-01", periods=5, freq="D")
|
||||
btc = pd.DataFrame({"value": [10, 11, 12, 13, 14]}, index=dates)
|
||||
m2 = pd.DataFrame({"value": [1, 2, 3, 4, 5]}, index=dates)
|
||||
return btc, m2
|
||||
|
||||
|
||||
def test_plot_bitcoin_m2_returns_figure():
|
||||
btc, m2 = sample_data()
|
||||
fig = plot_bitcoin_m2(btc, m2, 94, 1)
|
||||
assert len(fig.axes) == 2
|
||||
assert any(ax.get_lines() for ax in fig.axes)
|
||||
|
||||
|
||||
def test_btc_m2_main(monkeypatch):
|
||||
btc, m2 = sample_data()
|
||||
|
||||
def fake_fetch(*_args, **_kwargs):
|
||||
return btc, m2
|
||||
|
||||
monkeypatch.setattr("scripts.bitcoin_m2_chart.fetch_series", fake_fetch)
|
||||
fig = btc_m2_main(["--btc-series", "BTC", "--m2-series", "M2"])
|
||||
assert len(fig.axes) == 2
|
||||
|
||||
|
||||
def test_btc_m2_main_saves_output(tmp_path, monkeypatch):
|
||||
btc, m2 = sample_data()
|
||||
|
||||
def fake_fetch(*_args, **_kwargs):
|
||||
return btc, m2
|
||||
|
||||
monkeypatch.setattr("scripts.bitcoin_m2_chart.fetch_series", fake_fetch)
|
||||
out_file = tmp_path / "out.png"
|
||||
btc_m2_main(
|
||||
[
|
||||
"--btc-series",
|
||||
"BTC",
|
||||
"--m2-series",
|
||||
"M2",
|
||||
"--output",
|
||||
str(out_file),
|
||||
]
|
||||
)
|
||||
assert out_file.exists()
|
||||
Reference in new issue
Block a user