feat: integration tests, seed data, and smoke test script
Add integration tests for the news pipeline (test_news_pipeline.py) and trading flow (test_trading_flow.py) using real Redis with mocked FinBERT and Alpaca. Add seed_strategies.py to insert default strategies (momentum, mean_reversion, news_driven) with equal weights. Add smoke_test.sh for end-to-end stack validation. Update pyproject.toml with integration marker and scripts package discovery.
This commit is contained in:
parent
b255b3edbe
commit
e6ae4bdccd
7 changed files with 948 additions and 1 deletions
0
scripts/__init__.py
Normal file
0
scripts/__init__.py
Normal file
109
scripts/seed_strategies.py
Normal file
109
scripts/seed_strategies.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"""Seed default trading strategies.
|
||||
|
||||
Inserts three strategies with equal initial weights (0.333 each):
|
||||
- momentum
|
||||
- mean_reversion
|
||||
- news_driven
|
||||
|
||||
Usage:
|
||||
python -m scripts.seed_strategies
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from shared.config import BaseConfig
|
||||
from shared.db import create_db
|
||||
from shared.models.trading import Strategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Default strategies to seed
|
||||
DEFAULT_STRATEGIES = [
|
||||
{
|
||||
"name": "momentum",
|
||||
"description": (
|
||||
"Buy when price crosses above N-period SMA with increasing volume; "
|
||||
"sell when it crosses below."
|
||||
),
|
||||
"current_weight": 0.333,
|
||||
"active": True,
|
||||
},
|
||||
{
|
||||
"name": "mean_reversion",
|
||||
"description": (
|
||||
"Buy when RSI < 30 (oversold); sell when RSI > 70 (overbought). "
|
||||
"Signal strength proportional to RSI extremity."
|
||||
),
|
||||
"current_weight": 0.333,
|
||||
"active": True,
|
||||
},
|
||||
{
|
||||
"name": "news_driven",
|
||||
"description": (
|
||||
"Buy on strong positive sentiment (score > 0.7, confidence > 0.6); "
|
||||
"sell on strong negative. Decay factor for stale news (> 4 hours)."
|
||||
),
|
||||
"current_weight": 0.333,
|
||||
"active": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def seed(database_url: str | None = None) -> None:
|
||||
"""Insert default strategies if they do not already exist.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
database_url:
|
||||
Override for the database URL. If ``None``, the default from
|
||||
:class:`~shared.config.BaseConfig` is used.
|
||||
"""
|
||||
config = BaseConfig()
|
||||
if database_url:
|
||||
config.database_url = database_url
|
||||
|
||||
_engine, session_factory = create_db(config)
|
||||
|
||||
async with session_factory() as session:
|
||||
for strategy_data in DEFAULT_STRATEGIES:
|
||||
# Check if the strategy already exists by name
|
||||
result = await session.execute(
|
||||
select(Strategy).where(Strategy.name == strategy_data["name"])
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(
|
||||
"Strategy '%s' already exists (weight=%.3f), skipping",
|
||||
existing.name,
|
||||
existing.current_weight,
|
||||
)
|
||||
continue
|
||||
|
||||
strategy = Strategy(**strategy_data)
|
||||
session.add(strategy)
|
||||
logger.info(
|
||||
"Inserted strategy '%s' with weight %.3f",
|
||||
strategy_data["name"],
|
||||
strategy_data["current_weight"],
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
|
||||
await _engine.dispose()
|
||||
logger.info("Strategy seeding complete")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""CLI entry-point."""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
asyncio.run(seed())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
139
scripts/smoke_test.sh
Executable file
139
scripts/smoke_test.sh
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
#!/bin/bash
|
||||
# Smoke test for the full trading-bot Docker Compose stack.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/smoke_test.sh
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Docker Compose stack must be running (docker compose up -d)
|
||||
#
|
||||
# This script:
|
||||
# 1. Waits for services to become healthy
|
||||
# 2. Hits GET /health -> expects 200
|
||||
# 3. Hits GET /api/portfolio -> expects 401 (unauthenticated)
|
||||
# 4. Hits GET /api/strategies -> expects 401 (unauthenticated)
|
||||
# 5. Checks docker compose ps shows all services running
|
||||
# 6. Exits 0 on success, 1 on failure
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
API_BASE="${API_BASE:-http://localhost:8000}"
|
||||
DASHBOARD_BASE="${DASHBOARD_BASE:-http://localhost:3000}"
|
||||
MAX_RETRIES="${MAX_RETRIES:-30}"
|
||||
RETRY_INTERVAL="${RETRY_INTERVAL:-2}"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper functions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
log() {
|
||||
echo "[smoke-test] $*"
|
||||
}
|
||||
|
||||
pass() {
|
||||
log "PASS: $*"
|
||||
PASS=$((PASS + 1))
|
||||
}
|
||||
|
||||
fail() {
|
||||
log "FAIL: $*"
|
||||
FAIL=$((FAIL + 1))
|
||||
}
|
||||
|
||||
wait_for_endpoint() {
|
||||
local url="$1"
|
||||
local expected_code="$2"
|
||||
local description="$3"
|
||||
local attempt=0
|
||||
|
||||
while [ "$attempt" -lt "$MAX_RETRIES" ]; do
|
||||
attempt=$((attempt + 1))
|
||||
status_code=$(curl -s -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "000")
|
||||
if [ "$status_code" = "$expected_code" ]; then
|
||||
return 0
|
||||
fi
|
||||
log "Waiting for $description ($url) ... attempt $attempt/$MAX_RETRIES (got $status_code, want $expected_code)"
|
||||
sleep "$RETRY_INTERVAL"
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
check_endpoint() {
|
||||
local url="$1"
|
||||
local expected_code="$2"
|
||||
local description="$3"
|
||||
|
||||
status_code=$(curl -s -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "000")
|
||||
if [ "$status_code" = "$expected_code" ]; then
|
||||
pass "$description -> $status_code"
|
||||
else
|
||||
fail "$description -> expected $expected_code, got $status_code"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Wait for the API gateway health endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Waiting for API gateway to be healthy ..."
|
||||
if wait_for_endpoint "$API_BASE/health" "200" "API health"; then
|
||||
pass "API gateway is healthy"
|
||||
else
|
||||
fail "API gateway did not become healthy within timeout"
|
||||
log "Aborting — cannot run further checks without a healthy API"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Health check
|
||||
# ---------------------------------------------------------------------------
|
||||
check_endpoint "$API_BASE/health" "200" "GET /health"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Unauthenticated trading endpoints should return 401/403
|
||||
# ---------------------------------------------------------------------------
|
||||
check_endpoint "$API_BASE/api/portfolio" "401" "GET /api/portfolio (no auth)"
|
||||
check_endpoint "$API_BASE/api/strategies" "401" "GET /api/strategies (no auth)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Dashboard responds
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Checking dashboard ..."
|
||||
if wait_for_endpoint "$DASHBOARD_BASE/" "200" "Dashboard"; then
|
||||
pass "Dashboard is serving"
|
||||
else
|
||||
fail "Dashboard did not respond"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Docker Compose services status
|
||||
# ---------------------------------------------------------------------------
|
||||
log "Checking docker compose service status ..."
|
||||
if command -v docker &>/dev/null; then
|
||||
running_count=$(docker compose ps --format json 2>/dev/null | grep -c '"running"' || echo "0")
|
||||
if [ "$running_count" -gt 0 ]; then
|
||||
pass "docker compose shows $running_count running services"
|
||||
else
|
||||
fail "No running services found in docker compose ps"
|
||||
fi
|
||||
else
|
||||
log "SKIP: docker command not available"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
log "================================"
|
||||
log "Results: $PASS passed, $FAIL failed"
|
||||
log "================================"
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue