mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-04-25 01:58:54 +02:00
@@ -12,8 +12,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Map service types to provider classes
|
# Map service types to provider classes
|
||||||
PROVIDER_MAPPING = {
|
PROVIDER_MAPPING = {
|
||||||
"synth_finance": providers.SynthFinanceProvider,
|
|
||||||
"synth_finance_stock": providers.SynthFinanceStockProvider,
|
|
||||||
"coingecko_free": providers.CoinGeckoFreeProvider,
|
"coingecko_free": providers.CoinGeckoFreeProvider,
|
||||||
"coingecko_pro": providers.CoinGeckoProProvider,
|
"coingecko_pro": providers.CoinGeckoProProvider,
|
||||||
"transitive": providers.TransitiveRateProvider,
|
"transitive": providers.TransitiveRateProvider,
|
||||||
|
|||||||
@@ -13,70 +13,6 @@ from apps.currencies.exchange_rates.base import ExchangeRateProvider
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SynthFinanceProvider(ExchangeRateProvider):
|
|
||||||
"""Implementation for Synth Finance API (synthfinance.com)"""
|
|
||||||
|
|
||||||
BASE_URL = "https://api.synthfinance.com/rates/live"
|
|
||||||
rates_inverted = False # SynthFinance returns non-inverted rates
|
|
||||||
|
|
||||||
def __init__(self, api_key: str = None):
|
|
||||||
super().__init__(api_key)
|
|
||||||
self.session = requests.Session()
|
|
||||||
self.session.headers.update({"Authorization": f"Bearer {self.api_key}"})
|
|
||||||
|
|
||||||
def get_rates(
|
|
||||||
self, target_currencies: QuerySet, exchange_currencies: set
|
|
||||||
) -> List[Tuple[Currency, Currency, Decimal]]:
|
|
||||||
results = []
|
|
||||||
currency_groups = {}
|
|
||||||
for currency in target_currencies:
|
|
||||||
if currency.exchange_currency in exchange_currencies:
|
|
||||||
group = currency_groups.setdefault(currency.exchange_currency.code, [])
|
|
||||||
group.append(currency)
|
|
||||||
|
|
||||||
for base_currency, currencies in currency_groups.items():
|
|
||||||
try:
|
|
||||||
to_currencies = ",".join(
|
|
||||||
currency.code
|
|
||||||
for currency in currencies
|
|
||||||
if currency.code != base_currency
|
|
||||||
)
|
|
||||||
response = self.session.get(
|
|
||||||
f"{self.BASE_URL}",
|
|
||||||
params={"from": base_currency, "to": to_currencies},
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
rates = data["data"]["rates"]
|
|
||||||
|
|
||||||
for currency in currencies:
|
|
||||||
if currency.code == base_currency:
|
|
||||||
rate = Decimal("1")
|
|
||||||
else:
|
|
||||||
rate = Decimal(str(rates[currency.code]))
|
|
||||||
# Return the rate as is, without inversion
|
|
||||||
results.append((currency.exchange_currency, currency, rate))
|
|
||||||
|
|
||||||
credits_used = data["meta"]["credits_used"]
|
|
||||||
credits_remaining = data["meta"]["credits_remaining"]
|
|
||||||
logger.info(
|
|
||||||
f"Synth Finance API call: {credits_used} credits used, {credits_remaining} remaining"
|
|
||||||
)
|
|
||||||
except requests.RequestException as e:
|
|
||||||
logger.error(
|
|
||||||
f"Error fetching rates from Synth Finance API for base {base_currency}: {e}"
|
|
||||||
)
|
|
||||||
except KeyError as e:
|
|
||||||
logger.error(
|
|
||||||
f"Unexpected response structure from Synth Finance API for base {base_currency}: {e}"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
f"Unexpected error processing Synth Finance data for base {base_currency}: {e}"
|
|
||||||
)
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
class CoinGeckoFreeProvider(ExchangeRateProvider):
|
class CoinGeckoFreeProvider(ExchangeRateProvider):
|
||||||
"""Implementation for CoinGecko Free API"""
|
"""Implementation for CoinGecko Free API"""
|
||||||
|
|
||||||
@@ -152,71 +88,6 @@ class CoinGeckoProProvider(CoinGeckoFreeProvider):
|
|||||||
self.session.headers.update({"x-cg-pro-api-key": api_key})
|
self.session.headers.update({"x-cg-pro-api-key": api_key})
|
||||||
|
|
||||||
|
|
||||||
class SynthFinanceStockProvider(ExchangeRateProvider):
|
|
||||||
"""Implementation for Synth Finance API Real-Time Prices endpoint (synthfinance.com)"""
|
|
||||||
|
|
||||||
BASE_URL = "https://api.synthfinance.com/tickers"
|
|
||||||
rates_inverted = True
|
|
||||||
|
|
||||||
def __init__(self, api_key: str = None):
|
|
||||||
super().__init__(api_key)
|
|
||||||
self.session = requests.Session()
|
|
||||||
self.session.headers.update(
|
|
||||||
{"Authorization": f"Bearer {self.api_key}", "accept": "application/json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_rates(
|
|
||||||
self, target_currencies: QuerySet, exchange_currencies: set
|
|
||||||
) -> List[Tuple[Currency, Currency, Decimal]]:
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for currency in target_currencies:
|
|
||||||
if currency.exchange_currency not in exchange_currencies:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Same currency has rate of 1
|
|
||||||
if currency.code == currency.exchange_currency.code:
|
|
||||||
rate = Decimal("1")
|
|
||||||
results.append((currency.exchange_currency, currency, rate))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Fetch real-time price for this ticker
|
|
||||||
response = self.session.get(
|
|
||||||
f"{self.BASE_URL}/{currency.code}/real-time"
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
# Use fair market value as the rate
|
|
||||||
rate = Decimal(data["data"]["fair_market_value"])
|
|
||||||
results.append((currency.exchange_currency, currency, rate))
|
|
||||||
|
|
||||||
# Log API usage
|
|
||||||
credits_used = data["meta"]["credits_used"]
|
|
||||||
credits_remaining = data["meta"]["credits_remaining"]
|
|
||||||
logger.info(
|
|
||||||
f"Synth Finance API call for {currency.code}: {credits_used} credits used, {credits_remaining} remaining"
|
|
||||||
)
|
|
||||||
except requests.RequestException as e:
|
|
||||||
logger.error(
|
|
||||||
f"Error fetching rate from Synth Finance API for ticker {currency.code}: {e}",
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
except KeyError as e:
|
|
||||||
logger.error(
|
|
||||||
f"Unexpected response structure from Synth Finance API for ticker {currency.code}: {e}",
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
f"Unexpected error processing Synth Finance data for ticker {currency.code}: {e}",
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
class TransitiveRateProvider(ExchangeRateProvider):
|
class TransitiveRateProvider(ExchangeRateProvider):
|
||||||
"""Calculates exchange rates through paths of existing rates"""
|
"""Calculates exchange rates through paths of existing rates"""
|
||||||
|
|
||||||
@@ -463,6 +334,10 @@ class TwelveDataProvider(ExchangeRateProvider):
|
|||||||
|
|
||||||
logger.info(f"Successfully fetched rate for {symbol} from Twelve Data.")
|
logger.info(f"Successfully fetched rate for {symbol} from Twelve Data.")
|
||||||
|
|
||||||
|
time.sleep(
|
||||||
|
60
|
||||||
|
) # We sleep every pair as to not step over TwelveData's minute limit
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Error fetching rate from Twelve Data API for symbol {symbol}: {e}"
|
f"Error fetching rate from Twelve Data API for symbol {symbol}: {e}"
|
||||||
@@ -610,6 +485,10 @@ class TwelveDataMarketsProvider(ExchangeRateProvider):
|
|||||||
f"Successfully processed price for {asset.code} as {final_price} {target_exchange_currency.code}"
|
f"Successfully processed price for {asset.code} as {final_price} {target_exchange_currency.code}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
time.sleep(
|
||||||
|
60
|
||||||
|
) # We sleep every pair as to not step over TwelveData's minute limit
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"TwelveDataMarkets: API request failed for {code_value}: {e}"
|
f"TwelveDataMarkets: API request failed for {code_value}: {e}"
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Generated by Django 5.2.5 on 2025-08-17 06:25
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
# The new value we are migrating to
|
||||||
|
NEW_SERVICE_TYPE = "frankfurter"
|
||||||
|
# The old values we are deprecating
|
||||||
|
OLD_SERVICE_TYPE_TO_UPDATE = "synth_finance"
|
||||||
|
OLD_SERVICE_TYPE_TO_DELETE = "synth_finance_stock"
|
||||||
|
|
||||||
|
|
||||||
|
def forwards_func(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Forward migration:
|
||||||
|
- Deletes all ExchangeRateService instances with service_type 'synth_finance_stock'.
|
||||||
|
- Updates all ExchangeRateService instances with service_type 'synth_finance' to 'frankfurter'.
|
||||||
|
"""
|
||||||
|
ExchangeRateService = apps.get_model("currencies", "ExchangeRateService")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
# 1. Delete the SYNTH_FINANCE_STOCK entries
|
||||||
|
ExchangeRateService.objects.using(db_alias).filter(
|
||||||
|
service_type=OLD_SERVICE_TYPE_TO_DELETE
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
# 2. Update the SYNTH_FINANCE entries to FRANKFURTER
|
||||||
|
ExchangeRateService.objects.using(db_alias).filter(
|
||||||
|
service_type=OLD_SERVICE_TYPE_TO_UPDATE
|
||||||
|
).update(service_type=NEW_SERVICE_TYPE, api_key=None)
|
||||||
|
|
||||||
|
|
||||||
|
def backwards_func(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Backward migration: This operation is not safely reversible.
|
||||||
|
- We cannot know which 'frankfurter' services were originally 'synth_finance'.
|
||||||
|
- The deleted 'synth_finance_stock' services cannot be recovered.
|
||||||
|
We will leave this function empty to allow migrating backwards without doing anything.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
# Add the previous migration file here
|
||||||
|
("currencies", "0019_alter_exchangerateservice_service_type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forwards_func, reverse_code=backwards_func),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.5 on 2025-08-17 06:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('currencies', '0020_migrate_synth_finance_services'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='exchangerateservice',
|
||||||
|
name='service_type',
|
||||||
|
field=models.CharField(choices=[('coingecko_free', 'CoinGecko (Demo/Free)'), ('coingecko_pro', 'CoinGecko (Pro)'), ('transitive', 'Transitive (Calculated from Existing Rates)'), ('frankfurter', 'Frankfurter'), ('twelvedata', 'TwelveData'), ('twelvedatamarkets', 'TwelveData Markets')], max_length=255, verbose_name='Service Type'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -94,8 +94,6 @@ class ExchangeRateService(models.Model):
|
|||||||
"""Configuration for exchange rate services"""
|
"""Configuration for exchange rate services"""
|
||||||
|
|
||||||
class ServiceType(models.TextChoices):
|
class ServiceType(models.TextChoices):
|
||||||
SYNTH_FINANCE = "synth_finance", "Synth Finance"
|
|
||||||
SYNTH_FINANCE_STOCK = "synth_finance_stock", "Synth Finance Stock"
|
|
||||||
COINGECKO_FREE = "coingecko_free", "CoinGecko (Demo/Free)"
|
COINGECKO_FREE = "coingecko_free", "CoinGecko (Demo/Free)"
|
||||||
COINGECKO_PRO = "coingecko_pro", "CoinGecko (Pro)"
|
COINGECKO_PRO = "coingecko_pro", "CoinGecko (Pro)"
|
||||||
TRANSITIVE = "transitive", "Transitive (Calculated from Existing Rates)"
|
TRANSITIVE = "transitive", "Transitive (Calculated from Existing Rates)"
|
||||||
|
|||||||
Reference in New Issue
Block a user