50 minReal data snapshotExecuted reading edition

Compute currency returns with explicit quote units

Invert ECB foreign-units-per-euro quotes before calculating euro-valued price returns and historical associations.

Economics & finance · Lesson 04🔋 Offline core analysis⏱ 50 minEmbedded real data📦 numpy · pandas · matplotlib · IPython

Goal and how to run

This lesson assumes basic Python knowledge. Read each method and caption before changing parameters. AI exercises are optional; no AI account is needed for the core workflow. Data lives in notebook metadata, so there is no companion data download. Keep the notebook saved in the active working folder.

Your goal is to understand the research question, execute transparent calculations, check the results, and state the limits of the conclusion.

Setup and parameters

In [1] · Python
from pathlib import Path
import base64, gzip, hashlib, json, platform
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, HTML
from datetime import datetime
from IPython.display import display as _display
def display(value):
    if isinstance(value,pd.DataFrame):
        table=value.to_html(max_rows=12,escape=True)
        style="<style>.vibeit-readable-table{max-width:100%;overflow-x:auto}.vibeit-readable-table table{width:max-content;max-width:none;border-collapse:collapse}.vibeit-readable-table td,.vibeit-readable-table th{white-space:nowrap!important;word-break:normal!important;overflow-wrap:normal!important}</style>"
        return _display(HTML(style+'<div class="vibeit-readable-table">'+table+'</div>'))
    return _display(value)
RECIPE_ID='ec04-fx-returns'
OUTPUT_DIR=Path.cwd()/"results"/RECIPE_ID
plt.rcParams.update({"figure.dpi":120,"font.size":10,"axes.spines.top":False,"axes.spines.right":False,
                      "axes.prop_cycle":plt.cycler(color=["#18785f","#416aa6","#bc6541","#9974a5"])})
print("Python:",platform.python_version(),"; NumPy:",np.__version__,"; Pandas:",pd.__version__)
print("Working folder:",Path.cwd())
print("Core data mode: embedded snapshot; no network requests")
Python: 3.13.13 ; NumPy: 2.5.3 ; Pandas: 3.0.6
Working folder: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads
Core data mode: embedded snapshot; no network requests

Load the embedded snapshot

In [2] · Python
def load_snapshot(recipe_id, expected_sha256, snapshot_key):
    """Read embedded data from this notebook, including a renamed copy."""
    for path in sorted(Path.cwd().glob("*.ipynb")):
        try:
            notebook = json.loads(path.read_text(encoding="utf-8"))
            metadata = notebook.get("metadata", {}).get("vibeit_cookbook", {})
            blob = metadata.get("snapshots", {}).get(snapshot_key)
            if metadata.get("recipe_id") != recipe_id or not blob:
                continue
            if blob.get("sha256") != expected_sha256:
                continue
            raw = gzip.decompress(base64.b64decode(blob["gzip_base64"], validate=True))
            if hashlib.sha256(raw).hexdigest() != expected_sha256:
                raise ValueError("Embedded snapshot checksum mismatch: " + snapshot_key)
            return json.loads(raw)
        except (OSError, json.JSONDecodeError):
            continue
    raise FileNotFoundError(
        "Save/open the complete .ipynb in the active working folder. "
        "The embedded snapshot could not be located for " + recipe_id)

SNAPSHOT_HASHES={'ecb': '7afd344000313ec2b4f6fb2548894e2a4817dc3687c14f8de47937ebf7bba0ad'}
def snapshot(key):
    return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['ecb']

Read the method functions

In [3] · Python
def iso_datetimes(values):
    """Parse source ISO dates with the standard library; keep tables as ISO strings."""
    return [datetime.fromisoformat(str(value)) for value in values]

print("Method ready: iso_datetimes")
Method ready: iso_datetimes

Data and model boundary

Historical price-only currency examples use hypothetical daily rebalancing, excluding interest/carry, fees, execution costs and tax. They do not establish future performance or an investment recommendation. Source: ECB; original reference rates are available free of charge at the ECB. Inversions, returns and risk statistics are derived course calculations, not ECB statistics.

Steps

1. State quotation direction first

A quote gives foreign units per EUR. The euro value of one foreign unit is its inverse. Rebase those calculated prices to 100 for display.

In [4] · Python
ecb=snapshot("ecb");rates=pd.DataFrame(ecb["rows"])
rates["date"]=rates["Date"].astype(str);rates=rates.set_index("date").sort_index()
currencies=ecb["currency_order"];original_dates=len(rates);rates=rates[currencies].dropna()
assert rates.index.is_unique and rates.index.is_monotonic_increasing and (rates>0).all().all()
prices=1/rates;returns=prices.pct_change(fill_method=None).iloc[1:]
print(ecb["source_notice"])
print("Quote:",ecb["quote"],"; common complete dates:",len(rates),"of",original_dates)
display(rates.head(6))
indices=100*prices/prices.iloc[0]
fig,ax=plt.subplots(figsize=(9,5))
for code in currencies:ax.plot(iso_datetimes(indices.index),indices[code],label=code)
ax.legend()
ax.set(xlabel="ECB reference date",ylabel="EUR-valued foreign-unit index (first date=100)",title="Explicit inverted quotation direction");plt.tight_layout();plt.show()
Source: European Central Bank. The original reference rates are available free from the ECB. Course returns, inversions and risk estimates are derived calculations, not ECB statistics.
Quote: Foreign currency units per 1 EUR ; common complete dates: 1538 of 1538
USD GBP JPY CNY
date
2019-01-02 1.1397 0.90165 124.28 7.8165
2019-01-03 1.1348 0.90312 122.21 7.8019
2019-01-04 1.1403 0.89988 123.20 7.8280
2019-01-07 1.1445 0.89720 123.90 7.8421
2019-01-08 1.1440 0.89743 124.46 7.8405
2019-01-09 1.1455 0.89913 124.70 7.8226
Computed figure from this code cell

2. Inspect reference-date return distributions

Simple changes are percent on common observed dates. Do not add zero returns for absent calendar days. These are price changes, excluding interest/carry, fees and executable quotes.

In [5] · Python
fig,ax=plt.subplots(figsize=(8,5))
for code in currencies:ax.hist(100*returns[code],bins=60,alpha=.35,label=code)
ax.set(xlabel="Reference-date price return (%)",ylabel="Intervals",title="Frozen historical return distributions");ax.legend();plt.tight_layout();plt.show()
Computed figure from this code cell

3. Read historical log-return associations

Correlation describes this frozen period on common dates. It is dimensionless and does not establish a stable future hedge.

In [6] · Python
log_returns=np.log(prices/prices.shift(1)).iloc[1:];correlation=log_returns.corr()
fig,ax=plt.subplots(figsize=(6,5));im=ax.imshow(correlation,cmap="coolwarm",vmin=-1,vmax=1)
ax.set_xticks(range(4),currencies);ax.set_yticks(range(4),currencies)
for i in range(4):
 for j in range(4):ax.text(j,i,f"{correlation.iloc[i,j]:.2f}",ha="center",va="center")
fig.colorbar(im,ax=ax,label="Historical log-return correlation");plt.tight_layout();plt.show()
Computed figure from this code cell

4. Reconcile inversion and export denominations

Inverted-price log changes are the negative original-quote log changes. Preserve both raw quotes and derived prices so this unit convention is reproducible.

In [7] · Python
assert np.allclose(log_returns,-np.log(rates/rates.shift(1)).iloc[1:])
assert np.allclose(returns,np.expm1(log_returns)) and np.allclose(correlation,correlation.T)
OUTPUT_DIR.mkdir(parents=True,exist_ok=True);rates.to_csv(OUTPUT_DIR/"foreign-units-per-EUR.csv")
prices.to_csv(OUTPUT_DIR/"EUR-per-foreign-unit.csv");returns.to_csv(OUTPUT_DIR/"EUR-price-returns.csv")
print("Quotation identity and common dates verified; exports:",OUTPUT_DIR)
Quotation identity and common dates verified; exports: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads/results/ec04-fx-returns

Exercises and reference explanation

  1. Why invert the quote?
  2. What do missing calendar days mean?

Reference explanation. The inverse is EUR per foreign unit. Common observed intervals, not inserted calendar-day zeros, define returns.

Keep a copy before changing parameters. Compare old and new figures and record which inputs, calculations and conclusions changed.

Optional AI coding exercises

Prompt 1

Using NumPy/Pandas/Matplotlib, compare original and inverted log changes and annotate opposite signs.

Prompt 2

With the same packages, export a dated price-only cumulative index without interest claims.

Human acceptance. Inversion identities pass; original quotes, denominations and free ECB source attribution remain explicit.

Review the edited source, then manually run affected cells and subsequent checks. Ask the assistant to explain the purpose, packages and data provenance of its changes, and reconcile that explanation with actual output.

Optional online extension

In [8] · Python
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
    import requests
    try:
        response=requests.get('https://data-api.ecb.europa.eu/service/data/EXR/D.USD.EUR.SP00.A?startPeriod=2024-01-01&endPeriod=2024-12-31&format=csvdata',timeout=20)
        response.raise_for_status()
        print("Source:",response.url)
        print(response.text[:700])
    except requests.RequestException as error:
        print("Online extension unavailable:",type(error).__name__,str(error)[:160])
else:
    print("Online extension skipped; embedded core data unchanged")
Online extension skipped; embedded core data unchanged

Sources, snapshots and next steps

  • ECB SDMX euro reference exchange rates — ECB copyright / free reuse with accurate source attribution; data obtained free from ECB; derived returns explicitly labelled; retrieved 2026-10-09. SHA-256 5f39fc39f60606abafae26c00492d05388c7bdf29cce1922f173f99bd0273ce8. Retain 2019-01-01 through 2024-12-31 and USD, GBP, JPY, CNY; preserve missing values and quotation direction.

Next steps. Transfer these methods to your own public or authorized data while retaining input checks, recorded parameters and result validation. Document the new experimental design and method limits; this example does not validate a new experiment. Full data licenses and generation instructions accompany the cookbook.