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>
This commit is contained in:
commit
61a7884ee5
127 changed files with 13323 additions and 0 deletions
74
backend/app/workers/fx_sync.py
Normal file
74
backend/app/workers/fx_sync.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
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))
|
||||
Loading…
Add table
Add a link
Reference in a new issue