mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-05-20 06:37:24 +02:00
feat: internal code for automatic exchange rate fetching
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.currencies.exchange_rates.providers import (
|
||||
SynthFinanceProvider,
|
||||
CoinGeckoProvider,
|
||||
)
|
||||
from apps.currencies.models import ExchangeRateService, ExchangeRate, Currency
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Map service types to provider classes
|
||||
PROVIDER_MAPPING = {
|
||||
"synth_finance": SynthFinanceProvider,
|
||||
"coingecko": CoinGeckoProvider,
|
||||
}
|
||||
|
||||
|
||||
class ExchangeRateFetcher:
|
||||
@staticmethod
|
||||
def fetch_due_rates(force: bool = False) -> None:
|
||||
"""
|
||||
Fetch rates for all services that are due for update.
|
||||
|
||||
Args:
|
||||
force (bool): If True, fetches all active services regardless of their last fetch time.
|
||||
If False, only fetches services that are due according to their interval.
|
||||
"""
|
||||
services = ExchangeRateService.objects.filter(is_active=True)
|
||||
current_time = timezone.now()
|
||||
|
||||
for service in services:
|
||||
try:
|
||||
if force:
|
||||
logger.info(f"Force fetching rates for {service.name}")
|
||||
ExchangeRateFetcher._fetch_service_rates(service)
|
||||
continue
|
||||
|
||||
# Regular schedule-based fetching
|
||||
if service.last_fetch is None:
|
||||
logger.info(f"First fetch for service: {service.name}")
|
||||
ExchangeRateFetcher._fetch_service_rates(service)
|
||||
continue
|
||||
|
||||
# Calculate when the next fetch should occur
|
||||
next_fetch_due = service.last_fetch + timedelta(
|
||||
hours=service.fetch_interval_hours
|
||||
)
|
||||
|
||||
# Check if it's time for the next fetch
|
||||
if current_time >= next_fetch_due:
|
||||
logger.info(
|
||||
f"Fetching rates for {service.name}. "
|
||||
f"Last fetch: {service.last_fetch}, "
|
||||
f"Interval: {service.fetch_interval_hours}h"
|
||||
)
|
||||
ExchangeRateFetcher._fetch_service_rates(service)
|
||||
else:
|
||||
logger.debug(
|
||||
f"Skipping {service.name}. "
|
||||
f"Next fetch due at: {next_fetch_due}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking fetch schedule for {service.name}: {e}")
|
||||
|
||||
@staticmethod
|
||||
def _get_unique_currency_pairs(
|
||||
service: ExchangeRateService,
|
||||
) -> tuple[QuerySet, set]:
|
||||
"""
|
||||
Get unique currency pairs from both target_currencies and target_accounts
|
||||
Returns a tuple of (target_currencies QuerySet, exchange_currencies set)
|
||||
"""
|
||||
# Get currencies from target_currencies
|
||||
target_currencies = set(service.target_currencies.all())
|
||||
|
||||
# Add currencies from target_accounts
|
||||
for account in service.target_accounts.all():
|
||||
if account.currency and account.exchange_currency:
|
||||
target_currencies.add(account.currency)
|
||||
|
||||
# Convert back to QuerySet for compatibility with existing code
|
||||
target_currencies_qs = Currency.objects.filter(
|
||||
id__in=[curr.id for curr in target_currencies]
|
||||
)
|
||||
|
||||
# Get unique exchange currencies
|
||||
exchange_currencies = set()
|
||||
|
||||
# From target_currencies
|
||||
for currency in target_currencies:
|
||||
if currency.exchange_currency:
|
||||
exchange_currencies.add(currency.exchange_currency)
|
||||
|
||||
# From target_accounts
|
||||
for account in service.target_accounts.all():
|
||||
if account.exchange_currency:
|
||||
exchange_currencies.add(account.exchange_currency)
|
||||
|
||||
return target_currencies_qs, exchange_currencies
|
||||
|
||||
@staticmethod
|
||||
def _fetch_service_rates(service: ExchangeRateService) -> None:
|
||||
"""Fetch rates for a specific service"""
|
||||
try:
|
||||
provider = service.get_provider()
|
||||
|
||||
# Check if API key is required but missing
|
||||
if provider.requires_api_key() and not service.api_key:
|
||||
logger.error(f"API key required but not provided for {service.name}")
|
||||
return
|
||||
|
||||
# Get unique currency pairs from both sources
|
||||
target_currencies, exchange_currencies = (
|
||||
ExchangeRateFetcher._get_unique_currency_pairs(service)
|
||||
)
|
||||
|
||||
# Skip if no currencies to process
|
||||
if not target_currencies or not exchange_currencies:
|
||||
logger.info(f"No currency pairs to process for service {service.name}")
|
||||
return
|
||||
|
||||
rates = provider.get_rates(target_currencies, exchange_currencies)
|
||||
|
||||
# Track processed currency pairs to avoid duplicates
|
||||
processed_pairs = set()
|
||||
|
||||
for from_currency, to_currency, rate in rates:
|
||||
# Create a unique identifier for this currency pair
|
||||
pair_key = (from_currency.id, to_currency.id)
|
||||
if pair_key in processed_pairs:
|
||||
continue
|
||||
|
||||
if provider.rates_inverted:
|
||||
# If rates are inverted, we need to swap currencies
|
||||
ExchangeRate.objects.create(
|
||||
from_currency=to_currency,
|
||||
to_currency=from_currency,
|
||||
rate=rate,
|
||||
date=timezone.now(),
|
||||
)
|
||||
processed_pairs.add((to_currency.id, from_currency.id))
|
||||
else:
|
||||
# If rates are not inverted, we can use them as is
|
||||
ExchangeRate.objects.create(
|
||||
from_currency=from_currency,
|
||||
to_currency=to_currency,
|
||||
rate=rate,
|
||||
date=timezone.now(),
|
||||
)
|
||||
processed_pairs.add((from_currency.id, to_currency.id))
|
||||
|
||||
service.last_fetch = timezone.now()
|
||||
service.save()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching rates for {service.name}: {e}")
|
||||
Reference in New Issue
Block a user