Add initial project structure with Docker, CI, and FastAPI setup
CI / lint-test-build (push) Failing after 8m23s
CI / lint-test-build (push) Failing after 8m23s
- Create Dockerfile and docker-compose.yml for containerization - Add CI configuration for linting and testing - Implement FastAPI application with health check routes - Set up database schema using DuckDB - Include environment configuration and secrets management - Add README with project objectives and key features - Implement logging setup and configuration - Create initial tests for health route - Add HTML templates for web interface
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
.pytest_cache
|
||||||
|
.mypy_cache
|
||||||
|
.ruff_cache
|
||||||
|
data
|
||||||
|
logs
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
APP_ENV=dev
|
||||||
|
APP_HOST=0.0.0.0
|
||||||
|
APP_PORT=8000
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
LOG_JSON=true
|
||||||
|
DUCKDB_PATH=./data/arbitrade.duckdb
|
||||||
|
FERNET_KEY=
|
||||||
|
KRAKEN_API_KEY=
|
||||||
|
KRAKEN_API_SECRET=
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main", "master"]
|
||||||
|
tags: ["v*"]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-test-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install project + dev deps
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -e .[dev]
|
||||||
|
|
||||||
|
- name: Ruff
|
||||||
|
run: ruff check .
|
||||||
|
|
||||||
|
- name: Black
|
||||||
|
run: black --check .
|
||||||
|
|
||||||
|
- name: MyPy
|
||||||
|
run: mypy src
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
run: pytest -q
|
||||||
|
|
||||||
|
- name: Login to Gitea registry
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.allucanget.biz
|
||||||
|
username: ${{ secrets.GITEA_REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.GITEA_REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: git.allucanget.biz/${{ secrets.GITEA_REGISTRY_NAMESPACE }}/arbitrade:${{ github.sha }}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# Instructions
|
||||||
|
.github/instructions/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.so
|
||||||
|
*.egg-info/
|
||||||
|
.eggs/
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# IDE / OS
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env / secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
secrets/
|
||||||
|
|
||||||
|
# Local database / runtime data
|
||||||
|
data/*.duckdb
|
||||||
|
data/*.duckdb.wal
|
||||||
|
data/*.duckdb.tmp
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Node assets if used for frontend tooling
|
||||||
|
node_modules/
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.6.8
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
- id: ruff-format
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 24.8.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
|
rev: v1.11.2
|
||||||
|
hooks:
|
||||||
|
- id: mypy
|
||||||
|
additional_dependencies:
|
||||||
|
- pydantic>=2.9.0
|
||||||
|
- pydantic-settings>=2.5.0
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
FROM python:3.12-slim AS base
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip
|
||||||
|
|
||||||
|
COPY pyproject.toml README.md /app/
|
||||||
|
COPY src /app/src
|
||||||
|
COPY web /app/web
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir .
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["python", "-m", "arbitrade.main"]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Arbitrage Trading Bot
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
- Develop an arbitrage trading bot that can identify and exploit price discrepancies across different currency pairs to generate profits.
|
||||||
|
- Ensure the bot operates efficiently, securely, and can adapt to changing market conditions.
|
||||||
|
- Implement risk management strategies to minimize potential losses.
|
||||||
|
- Continuously monitor and optimize the bot's performance.
|
||||||
|
- Provide a user-friendly interface for monitoring and controlling the bot's operations.
|
||||||
|
- Integrate with kraken exchange for executing trades and accessing market data.
|
||||||
|
- Implement a robust logging and alerting system to track the bot's activities and notify the user of important events or issues.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
1. **Market Data Collection**: The bot will collect real-time market data, including price, volume, and order book information.
|
||||||
|
2. **Price Discrepancy Detection**: The bot will analyze the collected data to identify price discrepancies between currency pairs across different cryptocurrencies and fiat currencies.
|
||||||
|
3. **Trade Execution**: The bot will execute trades automatically when a profitable arbitrage opportunity is detected, ensuring that it can capitalize on the price differences before they disappear.
|
||||||
|
4. **Risk Management**: The bot will implement risk management strategies, such as setting stop-loss orders and limiting the amount of capital allocated to each trade, to minimize potential losses.
|
||||||
|
5. **Performance Monitoring**: The bot will continuously monitor its performance, tracking metrics such as profit and loss, win rate, and average trade duration, to identify areas for improvement.
|
||||||
|
6. **User Interface**: The bot will provide a user-friendly interface that allows users to monitor the bot's activities, view performance metrics, and control its operations.
|
||||||
|
7. **Integration with Kraken Exchange**: The bot will integrate with the Kraken exchange to access market data and execute trades, ensuring that it can operate effectively in the cryptocurrency market.
|
||||||
|
8. **Logging and Alerting System**: The bot will implement a robust logging system to track all activities and transactions, and an alerting system to notify the user of important events, such as successful trades, errors, or significant market changes.
|
||||||
|
9. **Security Measures**: The bot will implement security measures to protect user data and prevent unauthorized access, including encryption of sensitive information and secure authentication methods.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
arbitrade:
|
||||||
|
image: git.allucanget.biz/OWNER/arbitrade:latest
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling>=1.25.0"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "arbitrade"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Low-latency Kraken arbitrage bot"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"cryptography>=43.0.0",
|
||||||
|
"duckdb>=1.1.0",
|
||||||
|
"fastapi>=0.115.0",
|
||||||
|
"httptools>=0.6.1",
|
||||||
|
"httpx>=0.28.0",
|
||||||
|
"jinja2>=3.1.0",
|
||||||
|
"keyring>=25.0.0",
|
||||||
|
"orjson>=3.10.0",
|
||||||
|
"pydantic>=2.9.0",
|
||||||
|
"pydantic-settings>=2.5.0",
|
||||||
|
"structlog>=24.4.0",
|
||||||
|
"sortedcontainers>=2.4.0",
|
||||||
|
"uvicorn[standard]>=0.31.0",
|
||||||
|
"uvloop>=0.20.0; platform_system != 'Windows'",
|
||||||
|
"websockets>=13.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"black>=24.8.0",
|
||||||
|
"mypy>=1.11.0",
|
||||||
|
"pre-commit>=3.8.0",
|
||||||
|
"pytest>=8.3.0",
|
||||||
|
"pytest-asyncio>=0.24.0",
|
||||||
|
"ruff>=0.6.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
arbitrade = "arbitrade.main:main"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/arbitrade"]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 100
|
||||||
|
target-version = ["py312"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py312"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "UP", "B", "N", "ASYNC"]
|
||||||
|
ignore = ["E203"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.12"
|
||||||
|
strict = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
pretty = true
|
||||||
|
mypy_path = "src"
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = ["duckdb", "keyring", "uvloop"]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
asyncio_mode = "auto"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
__all__ = ["__version__"]
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from arbitrade.api.routes import router
|
||||||
|
from arbitrade.config.settings import Settings
|
||||||
|
from arbitrade.logging_setup import configure_logging
|
||||||
|
from arbitrade.storage.db import DuckDBStore
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(settings: Settings) -> FastAPI:
|
||||||
|
configure_logging(settings.log_level, settings.log_json)
|
||||||
|
|
||||||
|
db = DuckDBStore(settings)
|
||||||
|
db.migrate()
|
||||||
|
|
||||||
|
app = FastAPI(title="arbitrade", version="0.1.0")
|
||||||
|
app.include_router(router)
|
||||||
|
return app
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from fastapi.responses import HTMLResponse, JSONResponse
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
templates = Jinja2Templates(directory="web/templates")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_class=HTMLResponse)
|
||||||
|
async def home(request: Request) -> HTMLResponse:
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request=request,
|
||||||
|
name="health.html",
|
||||||
|
context={
|
||||||
|
"status": "ok",
|
||||||
|
"time": datetime.now(UTC).isoformat(),
|
||||||
|
"title": "Arbitrade Health",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health", response_class=JSONResponse)
|
||||||
|
async def health() -> JSONResponse:
|
||||||
|
return JSONResponse({"status": "ok", "service": "arbitrade"})
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import keyring
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class SecretStore:
|
||||||
|
service_name: str = "arbitrade"
|
||||||
|
|
||||||
|
def _load_or_create_key(self, key_env: str | None = None) -> bytes:
|
||||||
|
if key_env:
|
||||||
|
return key_env.encode("utf-8")
|
||||||
|
|
||||||
|
existing = keyring.get_password(self.service_name, "fernet_key")
|
||||||
|
if existing:
|
||||||
|
return existing.encode("utf-8")
|
||||||
|
|
||||||
|
generated = Fernet.generate_key()
|
||||||
|
keyring.set_password(self.service_name, "fernet_key", generated.decode("utf-8"))
|
||||||
|
return generated
|
||||||
|
|
||||||
|
def encrypt(self, plaintext: str, key_env: str | None = None) -> str:
|
||||||
|
key = self._load_or_create_key(key_env)
|
||||||
|
token = Fernet(key).encrypt(plaintext.encode("utf-8"))
|
||||||
|
return token.decode("utf-8")
|
||||||
|
|
||||||
|
def decrypt(self, ciphertext: str, key_env: str | None = None) -> str:
|
||||||
|
key = self._load_or_create_key(key_env)
|
||||||
|
value = Fernet(key).decrypt(ciphertext.encode("utf-8"))
|
||||||
|
return value.decode("utf-8")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_env_key() -> str:
|
||||||
|
return base64.urlsafe_b64encode(os.urandom(32)).decode("utf-8")
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
||||||
|
|
||||||
|
app_env: str = Field(default="dev", alias="APP_ENV")
|
||||||
|
app_host: str = Field(default="0.0.0.0", alias="APP_HOST")
|
||||||
|
app_port: int = Field(default=8000, alias="APP_PORT")
|
||||||
|
|
||||||
|
log_level: str = Field(default="INFO", alias="LOG_LEVEL")
|
||||||
|
log_json: bool = Field(default=True, alias="LOG_JSON")
|
||||||
|
|
||||||
|
duckdb_path: Path = Field(default=Path("./data/arbitrade.duckdb"), alias="DUCKDB_PATH")
|
||||||
|
|
||||||
|
fernet_key: str | None = Field(default=None, alias="FERNET_KEY")
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_settings() -> Settings:
|
||||||
|
return Settings()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
|
||||||
|
|
||||||
|
def configure_logging(log_level: str = "INFO", json_logs: bool = True) -> None:
|
||||||
|
level = getattr(logging, log_level.upper(), logging.INFO)
|
||||||
|
|
||||||
|
timestamper = structlog.processors.TimeStamper(fmt="iso", utc=True)
|
||||||
|
|
||||||
|
shared_processors: list[Any] = [
|
||||||
|
structlog.contextvars.merge_contextvars,
|
||||||
|
structlog.stdlib.add_log_level,
|
||||||
|
structlog.stdlib.add_logger_name,
|
||||||
|
timestamper,
|
||||||
|
]
|
||||||
|
|
||||||
|
if json_logs:
|
||||||
|
renderer: Any = structlog.processors.JSONRenderer()
|
||||||
|
else:
|
||||||
|
renderer = structlog.dev.ConsoleRenderer()
|
||||||
|
|
||||||
|
structlog.configure(
|
||||||
|
processors=[
|
||||||
|
*shared_processors,
|
||||||
|
structlog.processors.dict_tracebacks,
|
||||||
|
structlog.processors.EventRenamer("message"),
|
||||||
|
renderer,
|
||||||
|
],
|
||||||
|
wrapper_class=structlog.make_filtering_bound_logger(level),
|
||||||
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||||
|
cache_logger_on_first_use=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.basicConfig(format="%(message)s", stream=sys.stdout, level=level, force=True)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import platform
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from arbitrade.api.app import create_app
|
||||||
|
from arbitrade.config.settings import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
def _install_uvloop_if_available() -> None:
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
uvloop = import_module("uvloop")
|
||||||
|
uvloop.install()
|
||||||
|
except Exception:
|
||||||
|
# App can still run with default asyncio loop.
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
_install_uvloop_if_available()
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
|
app = create_app(settings)
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
app,
|
||||||
|
host=settings.app_host,
|
||||||
|
port=settings.app_port,
|
||||||
|
log_level=settings.log_level.lower(),
|
||||||
|
loop="uvloop" if platform.system() != "Windows" else "asyncio",
|
||||||
|
http="httptools",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import duckdb
|
||||||
|
|
||||||
|
from arbitrade.config.settings import Settings
|
||||||
|
|
||||||
|
SCHEMA_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||||
|
version INTEGER PRIMARY KEY,
|
||||||
|
applied_at TIMESTAMP DEFAULT current_timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS opportunities (
|
||||||
|
id UUID DEFAULT uuid(),
|
||||||
|
detected_at TIMESTAMP NOT NULL,
|
||||||
|
cycle VARCHAR NOT NULL,
|
||||||
|
gross_pct DOUBLE,
|
||||||
|
net_pct DOUBLE,
|
||||||
|
est_profit DOUBLE,
|
||||||
|
executed BOOLEAN DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS trades (
|
||||||
|
id UUID DEFAULT uuid(),
|
||||||
|
started_at TIMESTAMP NOT NULL,
|
||||||
|
finished_at TIMESTAMP,
|
||||||
|
status VARCHAR NOT NULL,
|
||||||
|
realized_pnl DOUBLE,
|
||||||
|
capital_used DOUBLE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS portfolio_snapshots (
|
||||||
|
snapshot_at TIMESTAMP NOT NULL,
|
||||||
|
balances JSON,
|
||||||
|
total_value_usd DOUBLE
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class DuckDBStore:
|
||||||
|
def __init__(self, settings: Settings) -> None:
|
||||||
|
self._db_path = Path(settings.duckdb_path)
|
||||||
|
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def connect(self) -> Iterator[duckdb.DuckDBPyConnection]:
|
||||||
|
conn = duckdb.connect(str(self._db_path))
|
||||||
|
try:
|
||||||
|
yield conn
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def migrate(self) -> None:
|
||||||
|
with self.connect() as conn:
|
||||||
|
conn.execute(SCHEMA_SQL)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import httpx
|
||||||
|
|
||||||
|
from arbitrade.api.app import create_app
|
||||||
|
from arbitrade.config.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
|
async def test_health_route_returns_ok() -> None:
|
||||||
|
app = create_app(Settings())
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
response = await client.get("/health")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == "ok"
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{{ title or "Arbitrade" }}</title>
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f4f7f5;
|
||||||
|
--ink: #122118;
|
||||||
|
--accent: #1f7a4c;
|
||||||
|
--card: #ffffff;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
color: var(--ink);
|
||||||
|
background: radial-gradient(circle at top, #e9f7ef, var(--bg));
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 4rem auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid #d8e7dd;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
box-shadow: 0 6px 24px rgba(18, 33, 24, 0.08);
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #d9f3e5;
|
||||||
|
color: var(--accent);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="wrap">{% block content %}{% endblock %}</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="card">
|
||||||
|
<h1>Arbitrade Bootstrap Complete</h1>
|
||||||
|
<p><span class="badge">Status: {{ status }}</span></p>
|
||||||
|
<p>UTC: {{ time }}</p>
|
||||||
|
<p>
|
||||||
|
Health JSON:
|
||||||
|
<a href="/health" hx-get="/health" hx-target="#health-json" hx-swap="innerHTML">refresh</a>
|
||||||
|
</p>
|
||||||
|
<pre id="health-json">{"status":"ok","service":"arbitrade"}</pre>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user