fix: Remove agreed_price from price API response

The agreed_price depends on trade direction (buy/sell) and must be
calculated on the frontend. Returning a buy-side-only agreed_price
from the API was misleading and unused.

Frontend already calculates the direction-aware price correctly.
This commit is contained in:
counterweight 2025-12-23 10:36:18 +01:00
parent 1008eea2d9
commit bf57fc6b77
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
7 changed files with 640 additions and 270 deletions

View file

@ -1,11 +1,15 @@
"""FastAPI application entry point."""
import asyncio
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from database import Base, engine
from database import Base, async_session, engine
from models import PriceHistory
from price_fetcher import PAIR_BTC_EUR, SOURCE_BITFINEX, fetch_btc_eur_price
from routes import audit as audit_routes
from routes import auth as auth_routes
from routes import availability as availability_routes
@ -13,19 +17,63 @@ from routes import exchange as exchange_routes
from routes import invites as invites_routes
from routes import meta as meta_routes
from routes import profile as profile_routes
from shared_constants import PRICE_REFRESH_SECONDS
from validate_constants import validate_shared_constants
logger = logging.getLogger(__name__)
# Background task handle
_price_fetch_task: asyncio.Task | None = None
async def periodic_price_fetcher():
"""Background task that fetches BTC/EUR price every minute."""
logger.info(
"Starting periodic price fetcher (every %d seconds)", PRICE_REFRESH_SECONDS
)
while True:
try:
price_value, timestamp = await fetch_btc_eur_price()
async with async_session() as db:
new_price = PriceHistory(
source=SOURCE_BITFINEX,
pair=PAIR_BTC_EUR,
price=price_value,
timestamp=timestamp,
)
db.add(new_price)
await db.commit()
logger.info("Fetched BTC/EUR price: €%.2f", price_value)
except Exception as e:
logger.error("Failed to fetch price: %s", e)
await asyncio.sleep(PRICE_REFRESH_SECONDS)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Create database tables on startup and validate constants."""
global _price_fetch_task
# Validate shared constants match backend definitions
validate_shared_constants()
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# Start background price fetcher
_price_fetch_task = asyncio.create_task(periodic_price_fetcher())
yield
# Cancel background task on shutdown
if _price_fetch_task:
_price_fetch_task.cancel()
try:
await _price_fetch_task
except asyncio.CancelledError:
logger.info("Price fetcher task cancelled")
app = FastAPI(lifespan=lifespan)

View file

@ -61,10 +61,13 @@ class ExchangeConfigResponse(BaseModel):
class PriceResponse(BaseModel):
"""Current BTC/EUR price with premium applied."""
"""Current BTC/EUR price for trading.
Note: The actual agreed price depends on trade direction (buy/sell)
and is calculated by the frontend using market_price and premium_percentage.
"""
market_price: float # Raw price from exchange
agreed_price: float # Price with premium applied
premium_percentage: int
timestamp: datetime
is_stale: bool
@ -115,13 +118,6 @@ def apply_premium_for_direction(
return market_price * (1 - premium_percentage / 100)
def apply_premium(market_price: float, premium_percentage: int) -> float:
"""Apply buy-side premium (for price display)."""
return apply_premium_for_direction(
market_price, premium_percentage, TradeDirection.BUY
)
def calculate_sats_amount(
eur_cents: int,
price_eur_per_btc: float,
@ -204,7 +200,7 @@ async def get_exchange_price(
The response includes:
- market_price: The raw price from the exchange
- agreed_price: The price with admin premium applied
- premium_percentage: The premium to apply to trades
- is_stale: Whether the price is older than 5 minutes
- config: Trading configuration (min/max EUR, increment)
"""
@ -237,7 +233,6 @@ async def get_exchange_price(
return ExchangePriceResponse(
price=PriceResponse(
market_price=price_value,
agreed_price=apply_premium(price_value, PREMIUM_PERCENTAGE),
premium_percentage=PREMIUM_PERCENTAGE,
timestamp=timestamp,
is_stale=False,
@ -250,9 +245,6 @@ async def get_exchange_price(
return ExchangePriceResponse(
price=PriceResponse(
market_price=cached_price.price,
agreed_price=apply_premium(
cached_price.price, PREMIUM_PERCENTAGE
),
premium_percentage=PREMIUM_PERCENTAGE,
timestamp=cached_price.timestamp,
is_stale=True,
@ -271,7 +263,6 @@ async def get_exchange_price(
return ExchangePriceResponse(
price=PriceResponse(
market_price=cached_price.price,
agreed_price=apply_premium(cached_price.price, PREMIUM_PERCENTAGE),
premium_percentage=PREMIUM_PERCENTAGE,
timestamp=cached_price.timestamp,
is_stale=is_price_stale(cached_price.timestamp),

View file

@ -98,8 +98,7 @@ class TestExchangePriceEndpoint:
assert "config" in data
assert data["price"]["market_price"] == 20000.0
assert data["price"]["premium_percentage"] == 5
# Agreed price should be market * 1.05 (5% premium)
assert data["price"]["agreed_price"] == pytest.approx(21000.0, rel=0.001)
# Note: agreed_price is calculated on frontend based on direction (buy/sell)
@pytest.mark.asyncio
async def test_admin_cannot_get_price(self, client_factory, admin_user):