MyMidas/backend/app/workers/fx_sync.py
megaproxy 61a7884ee5 Initial commit: MyMidas personal finance tracker
Full-stack self-hosted finance app with FastAPI backend and React frontend.

Features:
- Accounts, transactions, budgets, investments with GBP base currency
- CSV import with auto-detection for 10 UK bank formats
- ML predictions: spending forecast, net worth projection, Monte Carlo
- 7 selectable themes (Obsidian, Arctic, Midnight, Vault, Terminal, Synthwave, Ledger)
- Receipt/document attachments on transactions (JPEG, PNG, WebP, PDF)
- AES-256-GCM field encryption, RS256 JWT, TOTP 2FA, RLS, audit log
- Encrypted nightly backups + key rotation script
- Mobile-responsive layout with bottom nav

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 11:56:10 +00:00

74 lines
2.4 KiB
Python

import structlog
logger = structlog.get_logger()
PAIRS = [
("GBP", "USD"), ("GBP", "EUR"), ("GBP", "JPY"), ("GBP", "CAD"),
("GBP", "AUD"), ("GBP", "CHF"), ("USD", "GBP"), ("EUR", "GBP"),
]
async def fx_sync_job() -> None:
from app.dependencies import get_session_factory
session_factory = get_session_factory()
if not session_factory:
return
try:
import requests
r = requests.get(
"https://api.exchangerate-api.com/v4/latest/GBP",
timeout=10,
)
r.raise_for_status()
data = r.json()
rates = data.get("rates", {})
except Exception as exc:
logger.error("fx_fetch_failed", error=str(exc))
return
from datetime import datetime, timezone
from decimal import Decimal
import uuid as _uuid
from sqlalchemy import select
from app.db.models.currency import ExchangeRate # type: ignore[attr-defined]
async with session_factory() as db:
try:
now = datetime.now(timezone.utc)
for base, quote in PAIRS:
if base == "GBP":
rate_val = rates.get(quote)
else:
gbp_to_base = rates.get(base)
if not gbp_to_base or gbp_to_base == 0:
continue
rate_val = 1 / gbp_to_base
if not rate_val:
continue
result = await db.execute(
select(ExchangeRate).where(
ExchangeRate.base_currency == base,
ExchangeRate.quote_currency == quote,
)
)
existing = result.scalar_one_or_none()
if existing:
existing.rate = Decimal(str(round(rate_val, 8)))
existing.fetched_at = now
else:
db.add(ExchangeRate(
id=_uuid.uuid4(),
base_currency=base,
quote_currency=quote,
rate=Decimal(str(round(rate_val, 8))),
source="exchangerate-api",
fetched_at=now,
))
await db.commit()
logger.info("fx_sync_done", pairs=len(PAIRS))
except Exception as exc:
await db.rollback()
logger.error("fx_sync_db_failed", error=str(exc))