Step 6: Update exchange creation logic to use new pricing config

- Update ExchangeService to load pricing config from database
- Update validate_eur_amount to use direction-specific limits
- Update apply_premium_for_direction to calculate base + extra premium
- Update create_exchange to use new premium calculation
- Add tests for premium calculation (small trade extra, large trade base only, direction-specific)
- Update existing tests to account for new premium calculation
This commit is contained in:
counterweight 2025-12-26 20:24:13 +01:00
parent d317939ad0
commit 41e158376c
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
2 changed files with 210 additions and 26 deletions

View file

@ -25,13 +25,11 @@ from models import (
from repositories.availability import AvailabilityRepository
from repositories.exchange import ExchangeRepository
from repositories.price import PriceRepository
from repositories.pricing import PricingRepository
from schemas import AvailableSlotsResponse, BookableSlot
from shared_constants import (
EUR_TRADE_INCREMENT,
EUR_TRADE_MAX,
EUR_TRADE_MIN,
LIGHTNING_MAX_EUR,
PREMIUM_PERCENTAGE,
PRICE_STALENESS_SECONDS,
SLOT_DURATION_MINUTES,
)
@ -48,24 +46,53 @@ class ExchangeService:
self.price_repo = PriceRepository(db)
self.exchange_repo = ExchangeRepository(db)
self.availability_repo = AvailabilityRepository(db)
self.pricing_repo = PricingRepository(db)
def apply_premium_for_direction(
async def apply_premium_for_direction(
self,
market_price: float,
premium_percentage: int,
eur_amount: int,
direction: TradeDirection,
) -> float:
) -> tuple[float, int]:
"""
Apply premium to market price based on trade direction.
Apply premium to market price based on trade direction and amount.
The premium is always favorable to the admin:
- When user BUYS BTC: user pays MORE (market * (1 + premium/100))
- When user SELLS BTC: user receives LESS (market * (1 - premium/100))
Premium calculation:
- Base premium for direction (from pricing config)
- Add extra premium if trade amount <= small trade threshold
Returns:
Tuple of (agreed_price, total_premium_percentage)
"""
# Load pricing config
pricing_config = await self.pricing_repo.get_current()
if pricing_config is None:
raise ServiceUnavailableError("Pricing configuration not available")
# Get base premium for direction
base_premium = (
pricing_config.premium_buy
if direction == TradeDirection.BUY
else pricing_config.premium_sell
)
# Calculate total premium (base + extra if small trade)
if eur_amount <= pricing_config.small_trade_threshold_eur:
total_premium = base_premium + pricing_config.small_trade_extra_premium
else:
total_premium = base_premium
# Apply premium to market price
if direction == TradeDirection.BUY:
return market_price * (1 + premium_percentage / 100)
agreed_price = market_price * (1 + total_premium / 100)
else: # SELL
return market_price * (1 - premium_percentage / 100)
agreed_price = market_price * (1 - total_premium / 100)
return agreed_price, total_premium
def calculate_sats_amount(
self,
@ -147,12 +174,39 @@ class ExchangeService:
return cached_price
async def validate_eur_amount(self, eur_amount: int) -> None:
"""Validate EUR amount is within configured limits."""
if eur_amount < EUR_TRADE_MIN * 100:
raise BadRequestError(f"EUR amount must be at least €{EUR_TRADE_MIN}")
if eur_amount > EUR_TRADE_MAX * 100:
raise BadRequestError(f"EUR amount must be at most €{EUR_TRADE_MAX}")
async def validate_eur_amount(
self, eur_amount: int, direction: TradeDirection
) -> None:
"""Validate EUR amount is within configured limits for the given direction."""
# Load pricing config
pricing_config = await self.pricing_repo.get_current()
if pricing_config is None:
raise ServiceUnavailableError("Pricing configuration not available")
# Get direction-specific limits
eur_min = (
pricing_config.eur_min_buy
if direction == TradeDirection.BUY
else pricing_config.eur_min_sell
)
eur_max = (
pricing_config.eur_max_buy
if direction == TradeDirection.BUY
else pricing_config.eur_max_sell
)
if eur_amount < eur_min:
direction_str = direction.value.upper()
raise BadRequestError(
f"EUR amount must be at least €{eur_min / 100:.0f} "
f"for {direction_str} trades"
)
if eur_amount > eur_max:
direction_str = direction.value.upper()
raise BadRequestError(
f"EUR amount must be at most €{eur_max / 100:.0f} "
f"for {direction_str} trades"
)
if eur_amount % (EUR_TRADE_INCREMENT * 100) != 0:
raise BadRequestError(
f"EUR amount must be a multiple of €{EUR_TRADE_INCREMENT}"
@ -218,8 +272,8 @@ class ExchangeService:
f"Trade ID: {existing_trade.public_id}"
)
# Validate EUR amount
await self.validate_eur_amount(eur_amount)
# Validate EUR amount (direction-specific)
await self.validate_eur_amount(eur_amount, direction)
# Validate Lightning threshold
await self.validate_lightning_threshold(bitcoin_transfer_method, eur_amount)
@ -233,10 +287,11 @@ class ExchangeService:
# Get and validate price
cached_price = await self.validate_price_not_stale()
# Calculate agreed price based on direction
# Calculate agreed price based on direction and amount
# (includes premium calculation)
market_price = cached_price.price
agreed_price = self.apply_premium_for_direction(
market_price, PREMIUM_PERCENTAGE, direction
agreed_price, total_premium_percentage = await self.apply_premium_for_direction(
market_price, eur_amount, direction
)
# Calculate sats amount based on agreed price
@ -263,7 +318,7 @@ class ExchangeService:
sats_amount=sats_amount,
market_price_eur=market_price,
agreed_price_eur=agreed_price,
premium_percentage=PREMIUM_PERCENTAGE,
premium_percentage=total_premium_percentage,
status=ExchangeStatus.BOOKED,
)