Lukas Goldschmidt 1 mese fa
parent
commit
13c3041b0a

+ 15 - 0
src/exec_mcp/models.py

@@ -0,0 +1,15 @@
+from __future__ import annotations
+
+from pydantic import BaseModel, Field
+
+
+class AccountView(BaseModel):
+    id: str
+    display_name: str | None = None
+    venue: str | None = None
+    venue_account_ref: str | None = None
+    description: str | None = None
+    enabled: bool
+    metadata: str = Field(default="{}")
+    created_at: str | None = None
+    updated_at: str | None = None

+ 179 - 0
src/exec_mcp/repo.py

@@ -0,0 +1,179 @@
+from __future__ import annotations
+
+import json
+from datetime import datetime, timezone
+from sqlite3 import IntegrityError
+from uuid import uuid4
+
+from fastapi import HTTPException
+
+from .storage import get_connection
+
+
+def utc_now_iso() -> str:
+    return datetime.now(timezone.utc).isoformat()
+
+
+def list_accounts(*, venue: str | None = None, enabled_only: bool = True) -> list[dict]:
+    query = "SELECT id, display_name, venue, venue_account_ref, description, enabled, metadata_json, created_at, updated_at FROM accounts"
+    clauses: list[str] = []
+    params: list[str] = []
+
+    if venue:
+        clauses.append("venue = ?")
+        params.append(venue)
+    if enabled_only:
+        clauses.append("enabled = 1")
+
+    if clauses:
+        query += " WHERE " + " AND ".join(clauses)
+    query += " ORDER BY created_at ASC"
+
+    with get_connection() as conn:
+        rows = conn.execute(query, tuple(params)).fetchall()
+
+    return [
+        {
+            "id": row["id"],
+            "display_name": row["display_name"],
+            "venue": row["venue"],
+            "venue_account_ref": row["venue_account_ref"],
+            "description": row["description"],
+            "enabled": bool(row["enabled"]),
+            "metadata": row["metadata_json"],
+            "created_at": row["created_at"],
+            "updated_at": row["updated_at"],
+        }
+        for row in rows
+    ]
+
+
+def get_account(account_id: str) -> dict:
+    with get_connection() as conn:
+        row = conn.execute(
+            "SELECT id, display_name, venue, venue_account_ref, description, enabled, metadata_json, created_at, updated_at FROM accounts WHERE id = ?",
+            (account_id,),
+        ).fetchone()
+    if row is None:
+        raise HTTPException(status_code=404, detail="account not found")
+    return {
+        "id": row["id"],
+        "display_name": row["display_name"],
+        "venue": row["venue"],
+        "venue_account_ref": row["venue_account_ref"],
+        "description": row["description"],
+        "enabled": bool(row["enabled"]),
+        "metadata": row["metadata_json"],
+        "created_at": row["created_at"],
+        "updated_at": row["updated_at"],
+    }
+
+
+def get_account_secrets(account_id: str) -> dict:
+    with get_connection() as conn:
+        row = conn.execute(
+            "SELECT api_key, api_secret FROM account_secrets WHERE account_id = ?",
+            (account_id,),
+        ).fetchone()
+    if row is None:
+        raise HTTPException(status_code=404, detail="account secrets not found")
+    return {"api_key": row["api_key"], "api_secret": row["api_secret"]}
+
+
+def create_account(*, display_name: str, venue: str, venue_account_ref: str, api_key: str, api_secret: str, description: str | None = None, enabled: bool = True) -> dict:
+    account_id = str(uuid4())
+    now = utc_now_iso()
+    with get_connection() as conn:
+        try:
+            conn.execute(
+                """
+                INSERT INTO accounts (id, display_name, venue, venue_account_ref, description, enabled, created_at, updated_at)
+                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+                """,
+                (account_id, display_name, venue, venue_account_ref, description, int(enabled), now, now),
+            )
+            conn.execute(
+                """
+                INSERT INTO account_secrets (account_id, api_key, api_secret, created_at, updated_at)
+                VALUES (?, ?, ?, ?, ?)
+                """,
+                (account_id, api_key, api_secret, now, now),
+            )
+            conn.commit()
+        except IntegrityError as exc:
+            conn.rollback()
+            if "api_key" in str(exc).lower():
+                raise HTTPException(status_code=409, detail="api key already exists") from exc
+            raise
+    return {"id": account_id, "display_name": display_name, "venue": venue}
+
+
+def update_account(*, account_id: str, display_name: str | None = None, description: str | None = None, enabled: bool | None = None) -> dict:
+    now = utc_now_iso()
+    with get_connection() as conn:
+        row = conn.execute("SELECT id FROM accounts WHERE id = ?", (account_id,)).fetchone()
+        if row is None:
+            raise HTTPException(status_code=404, detail="account not found")
+        if display_name is not None:
+            conn.execute("UPDATE accounts SET display_name = ?, updated_at = ? WHERE id = ?", (display_name, now, account_id))
+        if description is not None:
+            conn.execute("UPDATE accounts SET description = ?, updated_at = ? WHERE id = ?", (description, now, account_id))
+        if enabled is not None:
+            conn.execute("UPDATE accounts SET enabled = ?, updated_at = ? WHERE id = ?", (int(enabled), now, account_id))
+        conn.commit()
+    return {"id": account_id, "updated": True}
+
+
+def delete_account(*, account_id: str) -> dict:
+    with get_connection() as conn:
+        deleted = conn.execute("DELETE FROM accounts WHERE id = ?", (account_id,)).rowcount
+        conn.commit()
+    if not deleted:
+        raise HTTPException(status_code=404, detail="account not found")
+    return {"id": account_id, "deleted": True}
+
+
+def cache_get(cache_key: str) -> dict | None:
+    now = utc_now_iso()
+    with get_connection() as conn:
+        row = conn.execute(
+            "SELECT payload_json FROM api_cache WHERE cache_key = ? AND expires_at > ?",
+            (cache_key, now),
+        ).fetchone()
+    if row is None:
+        return None
+    return json.loads(row["payload_json"])
+
+
+def cache_put(cache_key: str, payload: dict, ttl_seconds: int) -> None:
+    now_dt = datetime.now(timezone.utc)
+    fetched_at = now_dt.isoformat()
+    expires_at = datetime.fromtimestamp(now_dt.timestamp() + ttl_seconds, tz=timezone.utc).isoformat()
+    with get_connection() as conn:
+        conn.execute(
+            """
+            INSERT INTO api_cache (cache_key, payload_json, fetched_at, expires_at)
+            VALUES (?, ?, ?, ?)
+            ON CONFLICT(cache_key) DO UPDATE SET
+                payload_json=excluded.payload_json,
+                fetched_at=excluded.fetched_at,
+                expires_at=excluded.expires_at
+            """,
+            (cache_key, json.dumps(payload), fetched_at, expires_at),
+        )
+        conn.commit()
+
+
+def save_balance_snapshot(*, account_id: str, asset_code: str, balance: float, balance_value: float | None = None, value_currency: str | None = None) -> str:
+    snapshot_id = str(uuid4())
+    captured_at = utc_now_iso()
+    with get_connection() as conn:
+        conn.execute(
+            """
+            INSERT INTO balance_snapshots (id, account_id, asset_code, balance, balance_value, value_currency, captured_at)
+            VALUES (?, ?, ?, ?, ?, ?, ?)
+            """,
+            (snapshot_id, account_id, asset_code, balance, balance_value, value_currency, captured_at),
+        )
+        conn.commit()
+    return snapshot_id

+ 15 - 166
src/exec_mcp/server.py

@@ -1,24 +1,21 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
 from contextlib import asynccontextmanager
 from contextlib import asynccontextmanager
-from datetime import datetime, timezone
-from sqlite3 import IntegrityError
-from uuid import uuid4
 
 
 from fastapi import FastAPI, Form, HTTPException
 from fastapi import FastAPI, Form, HTTPException
 from fastapi.responses import HTMLResponse, RedirectResponse
 from fastapi.responses import HTMLResponse, RedirectResponse
 from fastmcp import FastMCP
 from fastmcp import FastMCP
-from pydantic import BaseModel, Field
 
 
-from .storage import get_connection, init_db
+from .models import AccountView
+from . import repo
+from .services_bitstamp import fetch_account_info as fetch_remote_account_info
+from .storage import init_db
 
 
-# Public MCP surface stays read-only.
 mcp = FastMCP("exec-mcp")
 mcp = FastMCP("exec-mcp")
 
 
 
 
 @asynccontextmanager
 @asynccontextmanager
 async def lifespan(_: FastAPI):
 async def lifespan(_: FastAPI):
-    # Initialize the local SQLite scaffold on startup.
     init_db()
     init_db()
     yield
     yield
 
 
@@ -27,18 +24,6 @@ app = FastAPI(title="exec-mcp", lifespan=lifespan)
 SUPPORTED_VENUES = {"bitstamp"}
 SUPPORTED_VENUES = {"bitstamp"}
 
 
 
 
-class AccountView(BaseModel):
-    id: str
-    display_name: str | None = None
-    venue: str | None = None
-    venue_account_ref: str | None = None
-    description: str | None = None
-    enabled: bool
-    metadata: str = Field(default="{}")
-    created_at: str | None = None
-    updated_at: str | None = None
-
-
 @app.get("/", response_class=HTMLResponse)
 @app.get("/", response_class=HTMLResponse)
 def http_root() -> str:
 def http_root() -> str:
     return """
     return """
@@ -67,7 +52,7 @@ def http_health() -> dict:
 
 
 @app.get("/dashboard", response_class=HTMLResponse)
 @app.get("/dashboard", response_class=HTMLResponse)
 def http_dashboard() -> str:
 def http_dashboard() -> str:
-    rows = list_accounts()
+    rows = list_accounts(enabled_only=False)
     options = "".join(f'<option value="{v}">{v}</option>' for v in sorted(SUPPORTED_VENUES))
     options = "".join(f'<option value="{v}">{v}</option>' for v in sorted(SUPPORTED_VENUES))
     table_rows = "".join(
     table_rows = "".join(
         f"""
         f"""
@@ -157,7 +142,7 @@ def http_dashboard_create_account(
     description: str | None = Form(None),
     description: str | None = Form(None),
     enabled: bool = Form(False),
     enabled: bool = Form(False),
 ) -> RedirectResponse:
 ) -> RedirectResponse:
-    create_account(
+    repo.create_account(
         display_name=display_name,
         display_name=display_name,
         venue=venue,
         venue=venue,
         venue_account_ref=venue_account_ref,
         venue_account_ref=venue_account_ref,
@@ -171,13 +156,7 @@ def http_dashboard_create_account(
 
 
 @app.get("/dashboard/accounts/{account_id}/edit", response_class=HTMLResponse)
 @app.get("/dashboard/accounts/{account_id}/edit", response_class=HTMLResponse)
 def http_dashboard_edit_account(account_id: str) -> str:
 def http_dashboard_edit_account(account_id: str) -> str:
-    with get_connection() as conn:
-        row = conn.execute(
-            "SELECT id, display_name, venue, venue_account_ref, description, enabled FROM accounts WHERE id = ?",
-            (account_id,),
-        ).fetchone()
-    if row is None:
-        raise HTTPException(status_code=404, detail="account not found")
+    row = repo.get_account(account_id)
     checked = "checked" if row["enabled"] else ""
     checked = "checked" if row["enabled"] else ""
     return f"""
     return f"""
     <html>
     <html>
@@ -205,157 +184,27 @@ def http_dashboard_update_account(
     description: str = Form(""),
     description: str = Form(""),
     enabled: bool = Form(False),
     enabled: bool = Form(False),
 ) -> RedirectResponse:
 ) -> RedirectResponse:
-    update_account(account_id=account_id, display_name=display_name, description=description or None, enabled=enabled)
+    repo.update_account(account_id=account_id, display_name=display_name, description=description or None, enabled=enabled)
     return RedirectResponse(url="/dashboard", status_code=303)
     return RedirectResponse(url="/dashboard", status_code=303)
 
 
 
 
 @app.post("/dashboard/accounts/{account_id}/delete")
 @app.post("/dashboard/accounts/{account_id}/delete")
 def http_dashboard_delete_account(account_id: str) -> RedirectResponse:
 def http_dashboard_delete_account(account_id: str) -> RedirectResponse:
-    delete_account(account_id=account_id)
+    repo.delete_account(account_id=account_id)
     return RedirectResponse(url="/dashboard", status_code=303)
     return RedirectResponse(url="/dashboard", status_code=303)
 
 
 
 
 @mcp.tool()
 @mcp.tool()
-def list_accounts(venue: str | None = None) -> list[dict]:
-    query = "SELECT id, display_name, venue, venue_account_ref, description, enabled, metadata_json, created_at, updated_at FROM accounts"
-    params: tuple = ()
-    if venue:
-        query += " WHERE venue = ?"
-        params = (venue,)
-    query += " ORDER BY created_at ASC"
-
-    with get_connection() as conn:
-        rows = conn.execute(query, params).fetchall()
-
-    return [
-        {
-            "id": row["id"],
-            "display_name": row["display_name"],
-            "venue": row["venue"],
-            "venue_account_ref": row["venue_account_ref"],
-            "description": row["description"],
-            "enabled": bool(row["enabled"]),
-            "metadata": row["metadata_json"],
-            "created_at": row["created_at"],
-            "updated_at": row["updated_at"],
-        }
-        for row in rows
-    ]
+def list_accounts(enabled_only: bool = True, venue: str | None = None) -> list[dict]:
+    return repo.list_accounts(venue=venue, enabled_only=enabled_only)
 
 
 
 
 @mcp.tool()
 @mcp.tool()
 def get_account_info(account_id: str) -> dict:
 def get_account_info(account_id: str) -> dict:
-    with get_connection() as conn:
-        account = conn.execute(
-            "SELECT id, display_name, venue, venue_account_ref, description, enabled, metadata_json FROM accounts WHERE id = ?",
-            (account_id,),
-        ).fetchone()
-        secrets = None
-        if account is not None:
-            secrets = conn.execute(
-                "SELECT api_key, api_secret FROM account_secrets WHERE account_id = ?",
-                (account["id"],),
-            ).fetchone()
-
-    if account is None:
-        raise HTTPException(status_code=404, detail="account not found")
-
-    return {
-        "id": account["id"],
-        "display_name": account["display_name"],
-        "venue": account["venue"],
-        "venue_account_ref": account["venue_account_ref"],
-        "description": account["description"],
-        "enabled": bool(account["enabled"]),
-        "metadata": account["metadata_json"],
-        "api_key_present": secrets is not None,
-        "api_secret_present": secrets is not None,
-    }
-
-
-def create_account(
-    display_name: str,
-    venue: str,
-    venue_account_ref: str,
-    api_key: str,
-    api_secret: str,
-    description: str | None = None,
-    enabled: bool = True,
-) -> dict:
-    if venue not in SUPPORTED_VENUES:
-        raise HTTPException(status_code=400, detail="unsupported venue")
-
-    account_pk = str(uuid4())
-    now = datetime.now(timezone.utc).isoformat()
-    with get_connection() as conn:
-        try:
-            conn.execute(
-                """
-                INSERT INTO accounts (id, display_name, venue, venue_account_ref, description, enabled, created_at, updated_at)
-                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
-                """,
-                (account_pk, display_name, venue, venue_account_ref, description, int(enabled), now, now),
-            )
-            conn.execute(
-                """
-                INSERT INTO account_secrets (account_id, api_key, api_secret, created_at, updated_at)
-                VALUES (?, ?, ?, ?, ?)
-                """,
-                (account_pk, api_key, api_secret, now, now),
-            )
-            conn.commit()
-        except IntegrityError as exc:
-            conn.rollback()
-            if "api_key" in str(exc).lower():
-                raise HTTPException(status_code=409, detail="api key already exists") from exc
-            raise
-
-    return {"id": account_pk, "display_name": display_name, "venue": venue}
-
-
-def update_account(
-    account_id: str,
-    display_name: str | None = None,
-    description: str | None = None,
-    enabled: bool | None = None,
-) -> dict:
-    now = datetime.now(timezone.utc).isoformat()
-    with get_connection() as conn:
-        row = conn.execute("SELECT id FROM accounts WHERE id = ?", (account_id,)).fetchone()
-        if row is None:
-            raise HTTPException(status_code=404, detail="account not found")
-        if display_name is not None:
-            conn.execute("UPDATE accounts SET display_name = ?, updated_at = ? WHERE id = ?", (display_name, now, account_id))
-        if description is not None:
-            conn.execute("UPDATE accounts SET description = ?, updated_at = ? WHERE id = ?", (description, now, account_id))
-        if enabled is not None:
-            conn.execute("UPDATE accounts SET enabled = ?, updated_at = ? WHERE id = ?", (int(enabled), now, account_id))
-        conn.commit()
-    return {"id": account_id, "updated": True}
-
-
-def delete_account(account_id: str) -> dict:
-    with get_connection() as conn:
-        deleted = conn.execute("DELETE FROM accounts WHERE id = ?", (account_id,)).rowcount
-        conn.commit()
-    if not deleted:
-        raise HTTPException(status_code=404, detail="account not found")
-    return {"id": account_id, "deleted": True}
-
-
-def record_balance(account_id: str, asset_code: str, balance_value: float) -> dict:
-    # Internal helper, called by execution sync logic when balances change.
-    now = datetime.now(timezone.utc).isoformat()
-    with get_connection() as conn:
-        account = conn.execute("SELECT id FROM accounts WHERE id = ?", (account_id,)).fetchone()
-        if account is None:
-            raise HTTPException(status_code=404, detail="account not found")
-        conn.execute(
-            "INSERT INTO balance_snapshots (account_id, asset_code, balance_value, captured_at) VALUES (?, ?, ?, ?)",
-            (account["id"], asset_code, balance_value, now),
-        )
-        conn.commit()
-    return {"id": account_id, "asset_code": asset_code, "balance_value": balance_value, "captured_at": now}
+    account = repo.get_account(account_id)
+    if account["venue"] == "bitstamp":
+        return fetch_remote_account_info(account_id)
+    raise HTTPException(status_code=400, detail="unsupported venue")
 
 
 
 
 def main() -> None:
 def main() -> None:

+ 91 - 0
src/exec_mcp/services_bitstamp.py

@@ -0,0 +1,91 @@
+from __future__ import annotations
+
+try:
+    import bitstamp.client
+except ModuleNotFoundError:  # allows test runs without the optional dependency
+    bitstamp = None  # type: ignore
+
+from . import repo
+
+BALANCE_CACHE_TTL_SECONDS = 20
+ACCOUNT_INFO_CACHE_TTL_SECONDS = 30
+
+
+def _require_client() -> None:
+    if bitstamp is None:
+        raise RuntimeError("bitstamp-python-client dependency is not installed")
+
+
+def _build_trading_client(account_id: str):
+    _require_client()
+    account = repo.get_account(account_id)
+    secrets = repo.get_account_secrets(account_id)
+    return bitstamp.client.Trading(
+        username=account["venue_account_ref"],
+        key=secrets["api_key"],
+        secret=secrets["api_secret"],
+    )
+
+
+def _normalize_account_balance_payload(payload: dict, account_id: str) -> list[dict]:
+    snapshots: list[dict] = []
+    for key, value in payload.items():
+        if key.endswith("_balance") and isinstance(value, (int, float, str)):
+            asset_code = key.removesuffix("_balance").upper()
+            try:
+                balance = float(value)
+            except ValueError:
+                continue
+            snapshots.append(
+                {
+                    "account_id": account_id,
+                    "asset_code": asset_code,
+                    "balance": balance,
+                    "balance_value": None,
+                    "value_currency": None,
+                }
+            )
+    return snapshots
+
+
+def fetch_account_balance(account_id: str) -> dict:
+    cache_key = f"bitstamp:account_balance:{account_id}"
+    cached = repo.cache_get(cache_key)
+    if cached is not None:
+        return cached
+
+    client = _build_trading_client(account_id)
+    payload = client.account_balance()
+    normalized = _normalize_account_balance_payload(payload, account_id)
+
+    for snapshot in normalized:
+        repo.save_balance_snapshot(**snapshot)
+
+    result = {"source": "bitstamp", "cached": False, "payload": payload, "normalized": normalized}
+    repo.cache_put(cache_key, result, BALANCE_CACHE_TTL_SECONDS)
+    return result
+
+
+def fetch_account_info(account_id: str) -> dict:
+    cache_key = f"bitstamp:account_info:{account_id}"
+    cached = repo.cache_get(cache_key)
+    if cached is not None:
+        return cached
+
+    account = repo.get_account(account_id)
+    balance = fetch_account_balance(account_id)
+
+    result = {
+        "id": account["id"],
+        "display_name": account["display_name"],
+        "venue": account["venue"],
+        "venue_account_ref": account["venue_account_ref"],
+        "description": account["description"],
+        "enabled": account["enabled"],
+        "metadata": account["metadata"],
+        "balance": balance["payload"],
+        "balance_normalized": balance["normalized"],
+    }
+
+    repo.cache_put(cache_key, result, ACCOUNT_INFO_CACHE_TTL_SECONDS)
+    return result

+ 7 - 0
src/exec_mcp/storage.py

@@ -65,6 +65,13 @@ def init_db() -> None:
                 raw_json TEXT NOT NULL DEFAULT '{}',
                 raw_json TEXT NOT NULL DEFAULT '{}',
                 FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE
                 FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE
             );
             );
+
+            CREATE TABLE IF NOT EXISTS api_cache (
+                cache_key TEXT PRIMARY KEY,
+                payload_json TEXT NOT NULL,
+                fetched_at TEXT NOT NULL,
+                expires_at TEXT NOT NULL
+            );
             """
             """
         )
         )
         conn.commit()
         conn.commit()

+ 8 - 3
tests.sh

@@ -6,10 +6,15 @@ cd "$ROOT_DIR"
 
 
 echo "Running tests (pytest)..."
 echo "Running tests (pytest)..."
 
 
-if command -v pytest >/dev/null 2>&1; then
-  exec pytest -q
-elif command -v python >/dev/null 2>&1; then
+if [[ -f .venv/bin/activate ]]; then
+  # shellcheck disable=SC1091
+  source .venv/bin/activate
+fi
+
+if command -v python >/dev/null 2>&1; then
   exec python -m pytest -q
   exec python -m pytest -q
+elif command -v pytest >/dev/null 2>&1; then
+  exec pytest -q
 else
 else
   echo "pytest/python not found in PATH. Install dependencies and retry." >&2
   echo "pytest/python not found in PATH. Install dependencies and retry." >&2
   exit 1
   exit 1

+ 22 - 13
tests/test_dashboard.py

@@ -1,6 +1,7 @@
 from fastapi.testclient import TestClient
 from fastapi.testclient import TestClient
 
 
 from exec_mcp.server import app
 from exec_mcp.server import app
+from exec_mcp.repo import list_accounts
 
 
 client = TestClient(app)
 client = TestClient(app)
 
 
@@ -13,32 +14,40 @@ def test_health():
 
 
 def test_dashboard_account_crud_roundtrip():
 def test_dashboard_account_crud_roundtrip():
     payload = {
     payload = {
-        'display_name': 'Bitstamp Main',
+        'display_name': 'synthetic-test-account',
         'venue': 'bitstamp',
         'venue': 'bitstamp',
-        'venue_account_ref': '123456',
-        'api_key': 'key-123',
-        'api_secret': 'secret-123',
-        'description': 'primary account',
+        'venue_account_ref': 'synthetic-ref-001',
+        'api_key': 'synthetic-api-key-001',
+        'api_secret': 'synthetic-api-secret-001',
+        'description': 'test row',
         'enabled': 'on',
         'enabled': 'on',
     }
     }
 
 
     resp = client.post('/dashboard/accounts/create', data=payload, follow_redirects=True)
     resp = client.post('/dashboard/accounts/create', data=payload, follow_redirects=True)
     assert resp.status_code == 200, resp.text
     assert resp.status_code == 200, resp.text
-    assert 'Bitstamp Main' in resp.text
-    assert '123456' in resp.text
+    assert 'synthetic-test-account' in resp.text
+    assert 'synthetic-ref-001' in resp.text
 
 
-    resp = client.get('/dashboard/accounts/bitstamp/123456/edit')
+    resp = client.get('/dashboard')
+    assert resp.status_code == 200
+    assert '/dashboard/accounts/' in resp.text
+
+    account_id = next(a['id'] for a in list_accounts(enabled_only=False) if a['venue_account_ref'] == 'synthetic-ref-001')
+
+    resp = client.get(f'/dashboard/accounts/{account_id}/edit')
     assert resp.status_code == 200
     assert resp.status_code == 200
     assert 'Edit account' in resp.text
     assert 'Edit account' in resp.text
 
 
     resp = client.post(
     resp = client.post(
-        '/dashboard/accounts/bitstamp/123456/update',
-        data={'display_name': 'Bitstamp Main', 'description': 'updated', 'enabled': 'on'},
+        f'/dashboard/accounts/{account_id}/update',
+        data={'display_name': 'synthetic-test-account-updated', 'description': 'updated', 'enabled': 'on'},
         follow_redirects=True,
         follow_redirects=True,
     )
     )
     assert resp.status_code == 200
     assert resp.status_code == 200
-    assert 'updated' in resp.text
+    assert 'synthetic-test-account-updated' in resp.text
 
 
-    resp = client.post('/dashboard/accounts/bitstamp/123456/delete', follow_redirects=True)
+    resp = client.post(f'/dashboard/accounts/{account_id}/delete', follow_redirects=True)
     assert resp.status_code == 200
     assert resp.status_code == 200
-    assert '123456' not in resp.text
+
+    remaining = list_accounts(enabled_only=False)
+    assert not any(a['id'] == account_id for a in remaining)