#!/usr/bin/env python3
"""
Brazil Central Bank (BCB) Economic Data Module — Phase 120
Data Sources:
- Banco Central do Brasil API (https://api.bcb.gov.br)
- Sistema Gerenciador de Séries Temporais (SGS)
- Covers SELIC rate, IPCA inflation, GDP, trade balance
BCB provides free JSON API for all Brazilian economic indicators
Series accessed via: https://api.bcb.gov.br/dados/serie/bcdata.sgs.{series_code}/dados?formato=json
Key Series Codes:
- 432: SELIC rate (% per annum)
- 433: SELIC target rate (% per annum)
- 11: IPCA (inflation index, monthly % change)
- 4380: GDP (quarterly, current prices, R$ millions)
- 22707: Trade balance (monthly, US$ millions FOB)
- 3696: Exchange rate (USD/BRL)
Author: QUANTCLAW DATA Build Agent
Phase: 120
"""
import requests
import json
import sys
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from statistics import mean
# BCB API Configuration
BCB_API_BASE = "https://api.bcb.gov.br/dados/serie/bcdata.sgs.{series}/dados"
BCB_TIMEOUT = 10
# Series codes mapping
BCB_SERIES = {
"SELIC": {"code": 432, "desc": "SELIC Rate - Effective (% per annum)", "unit": "%"},
"SELIC_TARGET": {"code": 433, "desc": "SELIC Target Rate (% per annum)", "unit": "%"},
"IPCA": {"code": 433, "desc": "IPCA - Consumer Price Index (monthly % change)", "unit": "%"},
"IPCA_12M": {"code": 13522, "desc": "IPCA - 12-month accumulated (% change)", "unit": "%"},
"GDP": {"code": 4380, "desc": "GDP at current prices (quarterly, R$ millions)", "unit": "R$ million"},
"GDP_REAL": {"code": 4385, "desc": "GDP real growth rate (quarterly YoY %)", "unit": "%"},
"TRADE_BALANCE": {"code": 22707, "desc": "Trade Balance (monthly, US$ million FOB)", "unit": "US$ million"},
"EXPORTS": {"code": 22710, "desc": "Exports (monthly, US$ million FOB)", "unit": "US$ million"},
"IMPORTS": {"code": 22709, "desc": "Imports (monthly, US$ million FOB)", "unit": "US$ million"},
"USDBRL": {"code": 3696, "desc": "Exchange Rate USD/BRL (daily)", "unit": "BRL"},
"FISCAL_RESULT": {"code": 2628, "desc": "Primary Fiscal Result (R$ millions)", "unit": "R$ million"},
"DEBT_GDP": {"code": 4513, "desc": "Public Debt / GDP ratio (%)", "unit": "%"},
}
# Fallback data for testing
FALLBACK_SELIC = {
"current_rate": 11.25,
"target_rate": 11.25,
"date": "2025-02-01",
"change_last_meeting": 0.25,
"next_meeting": "2025-03-19",
"historical_avg_12m": 11.65,
"interpretation": "Moderately restrictive to combat inflation"
}
FALLBACK_IPCA = {
"monthly_change": 0.42,
"month": "January 2025",
"accumulated_12m": 4.83,
"accumulated_ytd": 0.42,
"target": 3.00,
"tolerance_band": "1.5% - 4.5%",
"interpretation": "Above target midpoint but within tolerance"
}
FALLBACK_GDP = {
"value": 2650000,
"currency": "BRL",
"unit": "millions",
"quarter": "Q4 2024",
"real_growth_yoy": 2.8,
"real_growth_qoq": 0.7,
"annual_2024": 2.9,
"interpretation": "Resilient growth driven by agriculture and services"
}
FALLBACK_TRADE = {
"balance": 5280,
"exports": 28500,
"imports": 23220,
"currency": "USD",
"unit": "millions",
"month": "January 2025",
"balance_change_yoy": 15.2,
"exports_change_yoy": 8.5,
"imports_change_yoy": 5.1,
"interpretation": "Strong surplus driven by agricultural exports"
}
def fetch_bcb_series(series_code: int, start_date: Optional[str] = None, end_date: Optional[str] = None) -> List[Dict]:
"""
Fetch time series data from BCB API
Args:
series_code: BCB series code
start_date: Start date in DD/MM/YYYY format (optional)
end_date: End date in DD/MM/YYYY format (optional)
Returns:
List of data points with date and value
"""
url = BCB_API_BASE.format(series=series_code)
params = {"formato": "json"}
if start_date:
params["dataInicial"] = start_date
if end_date:
params["dataFinal"] = end_date
try:
response = requests.get(url, params=params, timeout=BCB_TIMEOUT)
response.raise_for_status()
data = response.json()
# BCB returns list of {data: "DD/MM/YYYY", valor: "123.45"}
return [{"date": item["data"], "value": float(item["valor"])} for item in data]
except Exception as e:
print(f"Error fetching BCB series {series_code}: {e}", file=sys.stderr)
return []
def get_selic_rate() -> Dict:
"""
Get current SELIC rate and target
Returns:
Dict with current rate, target, recent changes, interpretation
"""
try:
# Fetch last 30 days
end_date = datetime.now()
start_date = end_date - timedelta(days=30)
selic_data = fetch_bcb_series(
BCB_SERIES["SELIC"]["code"],
start_date=start_date.strftime("%d/%m/%Y"),
end_date=end_date.strftime("%d/%m/%Y")
)
target_data = fetch_bcb_series(
BCB_SERIES["SELIC_TARGET"]["code"],
start_date=start_date.strftime("%d/%m/%Y"),
end_date=end_date.strftime("%d/%m/%Y")
)
if not selic_data or not target_data:
print("Using fallback SELIC data", file=sys.stderr)
return FALLBACK_SELIC
current_rate = selic_data[-1]["value"]
target_rate = target_data[-1]["value"]
latest_date = selic_data[-1]["date"]
# Calculate 12-month average
year_ago = end_date - timedelta(days=365)
year_data = fetch_bcb_series(
BCB_SERIES["SELIC"]["code"],
start_date=year_ago.strftime("%d/%m/%Y"),
end_date=end_date.strftime("%d/%m/%Y")
)
avg_12m = mean([d["value"] for d in year_data]) if year_data else current_rate
# Determine stance
if current_rate >= 12.0:
interpretation = "Highly restrictive - aggressive inflation control"
elif current_rate >= 10.0:
interpretation = "Moderately restrictive to combat inflation"
elif current_rate >= 7.0:
interpretation = "Neutral - balancing growth and inflation"
else:
interpretation = "Accommodative - supporting economic growth"
return {
"current_rate": round(current_rate, 2),
"target_rate": round(target_rate, 2),
"date": latest_date,
"change_last_meeting": round(current_rate - selic_data[-2]["value"], 2) if len(selic_data) > 1 else 0,
"historical_avg_12m": round(avg_12m, 2),
"interpretation": interpretation,
"source": "Banco Central do Brasil"
}
except Exception as e:
print(f"Error in get_selic_rate: {e}", file=sys.stderr)
return FALLBACK_SELIC
def get_ipca_inflation() -> Dict:
"""
Get IPCA inflation data (Brazil's official CPI)
Returns:
Dict with monthly change, 12-month accumulated, target comparison
"""
try:
# Fetch last 13 months for 12M calculation
end_date = datetime.now()
start_date = end_date - timedelta(days=395)
ipca_monthly = fetch_bcb_series(
BCB_SERIES["IPCA"]["code"],
start_date=start_date.strftime("%d/%m/%Y"),
end_date=end_date.strftime("%d/%m/%Y")
)
ipca_12m = fetch_bcb_series(
BCB_SERIES["IPCA_12M"]["code"],
start_date=start_date.strftime("%d/%m/%Y"),
end_date=end_date.strftime("%d/%m/%Y")
)
if not ipca_monthly or not ipca_12m:
print("Using fallback IPCA data", file=sys.stderr)
return FALLBACK_IPCA
latest_monthly = ipca_monthly[-1]
latest_12m = ipca_12m[-1]
# Parse date
month_name = datetime.strptime(latest_monthly["date"], "%d/%m/%Y").strftime("%B %Y")
# Calculate YTD (current year)
current_year = datetime.now().year
ytd_data = [d for d in ipca_monthly if datetime.strptime(d["date"], "%d/%m/%Y").year == current_year]
ytd_accumulated = sum([d["value"] for d in ytd_data])
# Inflation target for Brazil (usually 3% with tolerance)
target = 3.00
tolerance_lower = 1.50
tolerance_upper = 4.50
accumulated_12m = latest_12m["value"]
if accumulated_12m < tolerance_lower:
interpretation = "Below tolerance band - deflation risk"
elif accumulated_12m > tolerance_upper:
interpretation = "Above tolerance band - inflation pressures"
elif accumulated_12m > target:
interpretation = "Above target midpoint but within tolerance"
else:
interpretation = "At or below target - stable prices"
return {
"monthly_change": round(latest_monthly["value"], 2),
"month": month_name,
"accumulated_12m": round(accumulated_12m, 2),
"accumulated_ytd": round(ytd_accumulated, 2),
"target": target,
"tolerance_band": f"{tolerance_lower}% - {tolerance_upper}%",
"interpretation": interpretation,
"source": "IBGE via BCB"
}
except Exception as e:
print(f"Error in get_ipca_inflation: {e}", file=sys.stderr)
return FALLBACK_IPCA
def get_gdp_data() -> Dict:
"""
Get GDP data (quarterly)
Returns:
Dict with latest GDP value, real growth rates, interpretation
"""
try:
# Fetch last 8 quarters (2 years)
end_date = datetime.now()
start_date = end_date - timedelta(days=730)
gdp_nominal = fetch_bcb_series(
BCB_SERIES["GDP"]["code"],
start_date=start_date.strftime("%d/%m/%Y"),
end_date=end_date.strftime("%d/%m/%Y")
)
gdp_real_growth = fetch_bcb_series(
BCB_SERIES["GDP_REAL"]["code"],
start_date=start_date.strftime("%d/%m/%Y"),
end_date=end_date.strftime("%d/%m/%Y")
)
if not gdp_nominal or not gdp_real_growth:
print("Using fallback GDP data", file=sys.stderr)
return FALLBACK_GDP
latest_nominal = gdp_nominal[-1]
latest_growth = gdp_real_growth[-1]
# Parse quarter
date_obj = datetime.strptime(latest_nominal["date"], "%d/%m/%Y")
quarter = f"Q{(date_obj.month - 1) // 3 + 1} {date_obj.year}"
# Calculate QoQ growth if available
qoq_growth = 0
if len(gdp_real_growth) > 1:
qoq_growth = gdp_real_growth[-1]["value"] - gdp_real_growth[-2]["value"]
# Annual growth (average of last 4 quarters)
annual_growth = mean([d["value"] for d in gdp_real_growth[-4:]]) if len(gdp_real_growth) >= 4 else latest_growth["value"]
# Interpretation
yoy = latest_growth["value"]
if yoy >= 4.0:
interpretation = "Strong expansion - robust economic growth"
elif yoy >= 2.0:
interpretation = "Moderate growth - stable expansion"
elif yoy >= 0:
interpretation = "Weak growth - near stagnation"
else:
interpretation = "Contraction - economic recession"
return {
"value": int(latest_nominal["value"]),
"currency": "BRL",
"unit": "millions",
"quarter": quarter,
"real_growth_yoy": round(yoy, 2),
"real_growth_qoq": round(qoq_growth, 2),
"annual_avg": round(annual_growth, 2),
"interpretation": interpretation,
"source": "IBGE via BCB"
}
except Exception as e:
print(f"Error in get_gdp_data: {e}", file=sys.stderr)
return FALLBACK_GDP
def get_trade_balance() -> Dict:
"""
Get trade balance data (monthly exports, imports, balance)
Returns:
Dict with balance, exports, imports, YoY changes
"""
try:
# Fetch last 13 months for YoY comparison
end_date = datetime.now()
start_date = end_date - timedelta(days=395)
balance_data = fetch_bcb_series(
BCB_SERIES["TRADE_BALANCE"]["code"],
start_date=start_date.strftime("%d/%m/%Y"),
end_date=end_date.strftime("%d/%m/%Y")
)
exports_data = fetch_bcb_series(
BCB_SERIES["EXPORTS"]["code"],
start_date=start_date.strftime("%d/%m/%Y"),
end_date=end_date.strftime("%d/%m/%Y")
)
imports_data = fetch_bcb_series(
BCB_SERIES["IMPORTS"]["code"],
start_date=start_date.strftime("%d/%m/%Y"),
end_date=end_date.strftime("%d/%m/%Y")
)
if not balance_data or not exports_data or not imports_data:
print("Using fallback trade data", file=sys.stderr)
return FALLBACK_TRADE
latest_balance = balance_data[-1]
latest_exports = exports_data[-1]
latest_imports = imports_data[-1]
# Parse month
month_name = datetime.strptime(latest_balance["date"], "%d/%m/%Y").strftime("%B %Y")
# Calculate YoY changes (12 months ago)
balance_yoy = 0
exports_yoy = 0
imports_yoy = 0
if len(balance_data) >= 13:
balance_yoy = ((latest_balance["value"] - balance_data[-13]["value"]) / abs(balance_data[-13]["value"])) * 100
exports_yoy = ((latest_exports["value"] - exports_data[-13]["value"]) / exports_data[-13]["value"]) * 100
imports_yoy = ((latest_imports["value"] - imports_data[-13]["value"]) / imports_data[-13]["value"]) * 100
# Interpretation
balance = latest_balance["value"]
if balance > 5000:
interpretation = "Strong surplus driven by commodity exports"
elif balance > 0:
interpretation = "Moderate surplus - positive trade contribution"
elif balance > -3000:
interpretation = "Small deficit - manageable trade gap"
else:
interpretation = "Large deficit - trade concerns"
return {
"balance": int(latest_balance["value"]),
"exports": int(latest_exports["value"]),
"imports": int(latest_imports["value"]),
"currency": "USD",
"unit": "millions",
"month": month_name,
"balance_change_yoy": round(balance_yoy, 2),
"exports_change_yoy": round(exports_yoy, 2),
"imports_change_yoy": round(imports_yoy, 2),
"interpretation": interpretation,
"source": "MDIC via BCB"
}
except Exception as e:
print(f"Error in get_trade_balance: {e}", file=sys.stderr)
return FALLBACK_TRADE
def get_brazil_dashboard() -> Dict:
"""
Get comprehensive Brazil economic dashboard
Returns:
Dict with SELIC, IPCA, GDP, trade balance, exchange rate
"""
return {
"country": "Brazil",
"data_date": datetime.now().strftime("%Y-%m-%d"),
"monetary_policy": get_selic_rate(),
"inflation": get_ipca_inflation(),
"gdp": get_gdp_data(),
"trade": get_trade_balance(),
"exchange_rate": get_exchange_rate(),
"source": "Banco Central do Brasil"
}
def get_exchange_rate() -> Dict:
"""
Get USD/BRL exchange rate
Returns:
Dict with current rate, recent changes
"""
try:
end_date = datetime.now()
start_date = end_date - timedelta(days=30)
fx_data = fetch_bcb_series(
BCB_SERIES["USDBRL"]["code"],
start_date=start_date.strftime("%d/%m/%Y"),
end_date=end_date.strftime("%d/%m/%Y")
)
if not fx_data:
return {"rate": 5.05, "date": end_date.strftime("%Y-%m-%d"), "change_1m": 0}
current = fx_data[-1]
change_1m = ((current["value"] - fx_data[0]["value"]) / fx_data[0]["value"]) * 100
return {
"rate": round(current["value"], 4),
"date": current["date"],
"change_1m_pct": round(change_1m, 2),
"pair": "USD/BRL"
}
except Exception as e:
print(f"Error fetching exchange rate: {e}", file=sys.stderr)
return {"rate": 5.05, "date": datetime.now().strftime("%Y-%m-%d"), "change_1m_pct": 0}
def main():
"""CLI handler"""
if len(sys.argv) < 2:
print("Usage: bcb.py <command>")
print("\nCommands:")
print(" brazil-selic - Get SELIC rate and target")
print(" brazil-ipca - Get IPCA inflation data")
print(" brazil-gdp - Get GDP data")
print(" brazil-trade-balance - Get trade balance")
print(" brazil-dashboard - Get comprehensive Brazil economic dashboard")
print(" brazil-exchange-rate - Get USD/BRL exchange rate")
sys.exit(1)
command = sys.argv[1]
if command == "brazil-selic":
result = get_selic_rate()
elif command == "brazil-ipca":
result = get_ipca_inflation()
elif command == "brazil-gdp":
result = get_gdp_data()
elif command == "brazil-trade-balance":
result = get_trade_balance()
elif command == "brazil-dashboard":
result = get_brazil_dashboard()
elif command == "brazil-exchange-rate":
result = get_exchange_rate()
else:
print(f"Unknown command: {command}", file=sys.stderr)
sys.exit(1)
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()