From 30e5d0828e62052739587195b67ed154d5ddc457 Mon Sep 17 00:00:00 2001 From: counterweight Date: Mon, 22 Dec 2025 18:16:35 +0100 Subject: [PATCH] Phase 1.1: Add exchange configuration - Add exchange constants to shared/constants.json: - eurTradeMin: 100, eurTradeMax: 3000, eurTradeIncrement: 20 - premiumPercentage: 5 - priceRefreshSeconds: 60, priceStalenessSeconds: 300 - Add exchangeStatuses and tradeDirections to shared constants - Add ExchangeStatus and TradeDirection enums to models.py - Update shared_constants.py to export new exchange constants - Update validate_constants.py to validate new enums and fields --- backend/models.py | 17 +++++++++++++++ backend/shared_constants.py | 8 +++++++ backend/validate_constants.py | 41 ++++++++++++++++++++++++++++++++++- shared/constants.json | 19 ++++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) diff --git a/backend/models.py b/backend/models.py index fe85b6d..bf593b9 100644 --- a/backend/models.py +++ b/backend/models.py @@ -68,6 +68,23 @@ class AppointmentStatus(str, PyEnum): CANCELLED_BY_ADMIN = "cancelled_by_admin" +class ExchangeStatus(str, PyEnum): + """Status of an exchange trade.""" + + BOOKED = "booked" + COMPLETED = "completed" + CANCELLED_BY_USER = "cancelled_by_user" + CANCELLED_BY_ADMIN = "cancelled_by_admin" + NO_SHOW = "no_show" + + +class TradeDirection(str, PyEnum): + """Direction of a trade from the user's perspective.""" + + BUY = "buy" # User buys BTC, gives EUR + SELL = "sell" # User sells BTC, gets EUR + + # Role name constants ROLE_ADMIN = "admin" ROLE_REGULAR = "regular" diff --git a/backend/shared_constants.py b/backend/shared_constants.py index b3151fc..4fe7c5a 100644 --- a/backend/shared_constants.py +++ b/backend/shared_constants.py @@ -11,3 +11,11 @@ SLOT_DURATION_MINUTES: int = _constants["booking"]["slotDurationMinutes"] MIN_ADVANCE_DAYS: int = _constants["booking"]["minAdvanceDays"] MAX_ADVANCE_DAYS: int = _constants["booking"]["maxAdvanceDays"] NOTE_MAX_LENGTH: int = _constants["booking"]["noteMaxLength"] + +# Exchange constants +EUR_TRADE_MIN: int = _constants["exchange"]["eurTradeMin"] +EUR_TRADE_MAX: int = _constants["exchange"]["eurTradeMax"] +EUR_TRADE_INCREMENT: int = _constants["exchange"]["eurTradeIncrement"] +PREMIUM_PERCENTAGE: int = _constants["exchange"]["premiumPercentage"] +PRICE_REFRESH_SECONDS: int = _constants["exchange"]["priceRefreshSeconds"] +PRICE_STALENESS_SECONDS: int = _constants["exchange"]["priceStalenessSeconds"] diff --git a/backend/validate_constants.py b/backend/validate_constants.py index 1ce7f0a..40c9202 100644 --- a/backend/validate_constants.py +++ b/backend/validate_constants.py @@ -3,7 +3,14 @@ import json from pathlib import Path -from models import ROLE_ADMIN, ROLE_REGULAR, AppointmentStatus, InviteStatus +from models import ( + ROLE_ADMIN, + ROLE_REGULAR, + AppointmentStatus, + ExchangeStatus, + InviteStatus, + TradeDirection, +) def validate_shared_constants() -> None: @@ -44,6 +51,24 @@ def validate_shared_constants() -> None: f"Expected: {expected_appointment_statuses}, Got: {got}" ) + # Validate exchange statuses + expected_exchange_statuses = {s.name: s.value for s in ExchangeStatus} + if constants.get("exchangeStatuses") != expected_exchange_statuses: + got = constants.get("exchangeStatuses") + raise ValueError( + f"Exchange status mismatch. " + f"Expected: {expected_exchange_statuses}, Got: {got}" + ) + + # Validate trade directions + expected_trade_directions = {d.name: d.value for d in TradeDirection} + if constants.get("tradeDirections") != expected_trade_directions: + got = constants.get("tradeDirections") + raise ValueError( + f"Trade direction mismatch. " + f"Expected: {expected_trade_directions}, Got: {got}" + ) + # Validate booking constants exist with required fields booking = constants.get("booking", {}) required_booking_fields = [ @@ -56,6 +81,20 @@ def validate_shared_constants() -> None: if field not in booking: raise ValueError(f"Missing booking constant '{field}' in constants.json") + # Validate exchange constants exist with required fields + exchange = constants.get("exchange", {}) + required_exchange_fields = [ + "eurTradeMin", + "eurTradeMax", + "eurTradeIncrement", + "premiumPercentage", + "priceRefreshSeconds", + "priceStalenessSeconds", + ] + for field in required_exchange_fields: + if field not in exchange: + raise ValueError(f"Missing exchange constant '{field}' in constants.json") + # Validate validation rules exist (structure check only) validation = constants.get("validation", {}) required_fields = ["telegram", "signal", "nostrNpub"] diff --git a/shared/constants.json b/shared/constants.json index 54f1086..5f7ee5f 100644 --- a/shared/constants.json +++ b/shared/constants.json @@ -13,12 +13,31 @@ "CANCELLED_BY_USER": "cancelled_by_user", "CANCELLED_BY_ADMIN": "cancelled_by_admin" }, + "exchangeStatuses": { + "BOOKED": "booked", + "COMPLETED": "completed", + "CANCELLED_BY_USER": "cancelled_by_user", + "CANCELLED_BY_ADMIN": "cancelled_by_admin", + "NO_SHOW": "no_show" + }, + "tradeDirections": { + "BUY": "buy", + "SELL": "sell" + }, "booking": { "slotDurationMinutes": 15, "maxAdvanceDays": 30, "minAdvanceDays": 1, "noteMaxLength": 144 }, + "exchange": { + "eurTradeMin": 100, + "eurTradeMax": 3000, + "eurTradeIncrement": 20, + "premiumPercentage": 5, + "priceRefreshSeconds": 60, + "priceStalenessSeconds": 300 + }, "validation": { "telegram": { "maxLengthAfterAt": 32,