diff --git a/API_REFERENCE.md b/API_REFERENCE.md new file mode 100644 index 0000000..2f789a8 --- /dev/null +++ b/API_REFERENCE.md @@ -0,0 +1,739 @@ +# AI-Trader API Reference + +Complete reference for the AI-Trader REST API service. + +**Base URL:** `http://localhost:8080` (default) + +**API Version:** 1.0.0 + +--- + +## Endpoints + +### POST /simulate/trigger + +Trigger a new simulation job for a specified date range and models. + +**Request Body:** + +```json +{ + "start_date": "2025-01-16", + "end_date": "2025-01-17", + "models": ["gpt-4", "claude-3.7-sonnet"] +} +``` + +**Parameters:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `start_date` | string | Yes | Start date in YYYY-MM-DD format | +| `end_date` | string | No | End date in YYYY-MM-DD format. If omitted, simulates single day (uses `start_date`) | +| `models` | array[string] | No | Model signatures to run. If omitted, uses all enabled models from server config | + +**Response (200 OK):** + +```json +{ + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "status": "pending", + "total_model_days": 4, + "message": "Simulation job created with 2 trading dates" +} +``` + +**Response Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `job_id` | string | Unique UUID for this simulation job | +| `status` | string | Job status: `pending`, `running`, `completed`, `partial`, or `failed` | +| `total_model_days` | integer | Total number of model-day combinations to execute | +| `message` | string | Human-readable status message | + +**Error Responses:** + +**400 Bad Request** - Invalid parameters or validation failure +```json +{ + "detail": "Invalid date format: 2025-1-16. Expected YYYY-MM-DD" +} +``` + +**400 Bad Request** - Another job is already running +```json +{ + "detail": "Another simulation job is already running or pending. Please wait for it to complete." +} +``` + +**500 Internal Server Error** - Server configuration issue +```json +{ + "detail": "Server configuration file not found: configs/default_config.json" +} +``` + +**503 Service Unavailable** - Price data download failed +```json +{ + "detail": "Failed to download any price data. Check ALPHAADVANTAGE_API_KEY." +} +``` + +**Validation Rules:** + +- **Date format:** Must be YYYY-MM-DD +- **Date validity:** Must be valid calendar dates +- **Date order:** `start_date` must be <= `end_date` +- **Future dates:** Cannot simulate future dates (must be <= today) +- **Date range limit:** Maximum 30 days (configurable via `MAX_SIMULATION_DAYS`) +- **Model signatures:** Must match models defined in server configuration +- **Concurrency:** Only one simulation job can run at a time + +**Behavior:** + +1. Validates date range and parameters +2. Determines which models to run (from request or server config) +3. Checks for missing price data in date range +4. Downloads missing data if `AUTO_DOWNLOAD_PRICE_DATA=true` (default) +5. Identifies trading dates with complete price data (all symbols available) +6. Creates job in database with status `pending` +7. Starts background worker thread +8. Returns immediately with job ID + +**Examples:** + +Single day, single model: +```bash +curl -X POST http://localhost:8080/simulate/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "start_date": "2025-01-16", + "models": ["gpt-4"] + }' +``` + +Date range, all enabled models: +```bash +curl -X POST http://localhost:8080/simulate/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "start_date": "2025-01-16", + "end_date": "2025-01-20" + }' +``` + +--- + +### GET /simulate/status/{job_id} + +Get status and progress of a simulation job. + +**URL Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `job_id` | string | Job UUID from trigger response | + +**Response (200 OK):** + +```json +{ + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "status": "running", + "progress": { + "total_model_days": 4, + "completed": 2, + "failed": 0, + "pending": 2 + }, + "date_range": ["2025-01-16", "2025-01-17"], + "models": ["gpt-4", "claude-3.7-sonnet"], + "created_at": "2025-01-16T10:00:00Z", + "started_at": "2025-01-16T10:00:05Z", + "completed_at": null, + "total_duration_seconds": null, + "error": null, + "details": [ + { + "model_signature": "gpt-4", + "trading_date": "2025-01-16", + "status": "completed", + "start_time": "2025-01-16T10:00:05Z", + "end_time": "2025-01-16T10:05:23Z", + "duration_seconds": 318.5, + "error": null + }, + { + "model_signature": "claude-3.7-sonnet", + "trading_date": "2025-01-16", + "status": "completed", + "start_time": "2025-01-16T10:05:24Z", + "end_time": "2025-01-16T10:10:12Z", + "duration_seconds": 288.0, + "error": null + }, + { + "model_signature": "gpt-4", + "trading_date": "2025-01-17", + "status": "running", + "start_time": "2025-01-16T10:10:13Z", + "end_time": null, + "duration_seconds": null, + "error": null + }, + { + "model_signature": "claude-3.7-sonnet", + "trading_date": "2025-01-17", + "status": "pending", + "start_time": null, + "end_time": null, + "duration_seconds": null, + "error": null + } + ] +} +``` + +**Response Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `job_id` | string | Job UUID | +| `status` | string | Overall job status | +| `progress` | object | Progress summary | +| `progress.total_model_days` | integer | Total model-day combinations | +| `progress.completed` | integer | Successfully completed model-days | +| `progress.failed` | integer | Failed model-days | +| `progress.pending` | integer | Not yet started model-days | +| `date_range` | array[string] | Trading dates in this job | +| `models` | array[string] | Model signatures in this job | +| `created_at` | string | ISO 8601 timestamp when job was created | +| `started_at` | string | ISO 8601 timestamp when execution began | +| `completed_at` | string | ISO 8601 timestamp when job finished | +| `total_duration_seconds` | float | Total execution time in seconds | +| `error` | string | Error message if job failed | +| `details` | array[object] | Per model-day execution details | + +**Job Status Values:** + +| Status | Description | +|--------|-------------| +| `pending` | Job created, waiting to start | +| `running` | Job currently executing | +| `completed` | All model-days completed successfully | +| `partial` | Some model-days completed, some failed | +| `failed` | All model-days failed | + +**Model-Day Status Values:** + +| Status | Description | +|--------|-------------| +| `pending` | Not started yet | +| `running` | Currently executing | +| `completed` | Finished successfully | +| `failed` | Execution failed (see `error` field) | + +**Error Response:** + +**404 Not Found** - Job doesn't exist +```json +{ + "detail": "Job 550e8400-e29b-41d4-a716-446655440000 not found" +} +``` + +**Example:** + +```bash +curl http://localhost:8080/simulate/status/550e8400-e29b-41d4-a716-446655440000 +``` + +**Polling Recommendation:** + +Poll every 10-30 seconds until `status` is `completed`, `partial`, or `failed`. + +--- + +### GET /results + +Query simulation results with optional filters. + +**Query Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `job_id` | string | No | Filter by job UUID | +| `date` | string | No | Filter by trading date (YYYY-MM-DD) | +| `model` | string | No | Filter by model signature | + +**Response (200 OK):** + +```json +{ + "results": [ + { + "id": 1, + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "date": "2025-01-16", + "model": "gpt-4", + "action_id": 1, + "action_type": "buy", + "symbol": "AAPL", + "amount": 10, + "price": 250.50, + "cash": 7495.00, + "portfolio_value": 10000.00, + "daily_profit": 0.00, + "daily_return_pct": 0.00, + "created_at": "2025-01-16T10:05:23Z", + "holdings": [ + {"symbol": "AAPL", "quantity": 10}, + {"symbol": "CASH", "quantity": 7495.00} + ] + }, + { + "id": 2, + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "date": "2025-01-16", + "model": "gpt-4", + "action_id": 2, + "action_type": "buy", + "symbol": "MSFT", + "amount": 5, + "price": 380.20, + "cash": 5594.00, + "portfolio_value": 10105.00, + "daily_profit": 105.00, + "daily_return_pct": 1.05, + "created_at": "2025-01-16T10:05:23Z", + "holdings": [ + {"symbol": "AAPL", "quantity": 10}, + {"symbol": "MSFT", "quantity": 5}, + {"symbol": "CASH", "quantity": 5594.00} + ] + } + ], + "count": 2 +} +``` + +**Response Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `results` | array[object] | Array of position records | +| `count` | integer | Number of results returned | + +**Position Record Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `id` | integer | Unique position record ID | +| `job_id` | string | Job UUID this belongs to | +| `date` | string | Trading date (YYYY-MM-DD) | +| `model` | string | Model signature | +| `action_id` | integer | Action sequence number (1, 2, 3...) for this model-day | +| `action_type` | string | Action taken: `buy`, `sell`, or `hold` | +| `symbol` | string | Stock symbol traded (or null for `hold`) | +| `amount` | integer | Quantity traded (or null for `hold`) | +| `price` | float | Price per share (or null for `hold`) | +| `cash` | float | Cash balance after this action | +| `portfolio_value` | float | Total portfolio value (cash + holdings) | +| `daily_profit` | float | Profit/loss for this trading day | +| `daily_return_pct` | float | Return percentage for this day | +| `created_at` | string | ISO 8601 timestamp when recorded | +| `holdings` | array[object] | Current holdings after this action | + +**Holdings Object:** + +| Field | Type | Description | +|-------|------|-------------| +| `symbol` | string | Stock symbol or "CASH" | +| `quantity` | float | Shares owned (or cash amount) | + +**Examples:** + +All results for a specific job: +```bash +curl "http://localhost:8080/results?job_id=550e8400-e29b-41d4-a716-446655440000" +``` + +Results for a specific date: +```bash +curl "http://localhost:8080/results?date=2025-01-16" +``` + +Results for a specific model: +```bash +curl "http://localhost:8080/results?model=gpt-4" +``` + +Combine filters: +```bash +curl "http://localhost:8080/results?job_id=550e8400-e29b-41d4-a716-446655440000&date=2025-01-16&model=gpt-4" +``` + +--- + +### GET /health + +Health check endpoint for monitoring and orchestration services. + +**Response (200 OK):** + +```json +{ + "status": "healthy", + "database": "connected", + "timestamp": "2025-01-16T10:00:00Z" +} +``` + +**Response Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `status` | string | Overall service health: `healthy` or `unhealthy` | +| `database` | string | Database connection status: `connected` or `disconnected` | +| `timestamp` | string | ISO 8601 timestamp of health check | + +**Example:** + +```bash +curl http://localhost:8080/health +``` + +**Usage:** + +- Docker health checks: `HEALTHCHECK CMD curl -f http://localhost:8080/health` +- Monitoring systems: Poll every 30-60 seconds +- Orchestration services: Verify availability before triggering simulations + +--- + +## Common Workflows + +### Trigger and Monitor a Simulation + +1. **Trigger simulation:** +```bash +RESPONSE=$(curl -X POST http://localhost:8080/simulate/trigger \ + -H "Content-Type: application/json" \ + -d '{"start_date": "2025-01-16", "end_date": "2025-01-17", "models": ["gpt-4"]}') + +JOB_ID=$(echo $RESPONSE | jq -r '.job_id') +echo "Job ID: $JOB_ID" +``` + +2. **Poll for completion:** +```bash +while true; do + STATUS=$(curl -s http://localhost:8080/simulate/status/$JOB_ID | jq -r '.status') + echo "Status: $STATUS" + + if [[ "$STATUS" == "completed" ]] || [[ "$STATUS" == "partial" ]] || [[ "$STATUS" == "failed" ]]; then + break + fi + + sleep 10 +done +``` + +3. **Retrieve results:** +```bash +curl "http://localhost:8080/results?job_id=$JOB_ID" | jq '.' +``` + +### Scheduled Daily Simulations + +Use a scheduler (cron, Airflow, etc.) to trigger simulations: + +```bash +#!/bin/bash +# daily_simulation.sh + +# Calculate yesterday's date +DATE=$(date -d "yesterday" +%Y-%m-%d) + +# Trigger simulation +curl -X POST http://localhost:8080/simulate/trigger \ + -H "Content-Type: application/json" \ + -d "{\"start_date\": \"$DATE\", \"models\": [\"gpt-4\"]}" +``` + +Add to crontab: +``` +0 6 * * * /path/to/daily_simulation.sh +``` + +--- + +## Error Handling + +All endpoints return consistent error responses with HTTP status codes and detail messages. + +### Common Error Codes + +| Code | Meaning | Common Causes | +|------|---------|---------------| +| 400 | Bad Request | Invalid date format, invalid parameters, concurrent job running | +| 404 | Not Found | Job ID doesn't exist | +| 500 | Internal Server Error | Server misconfiguration, missing config file | +| 503 | Service Unavailable | Price data download failed, database unavailable | + +### Error Response Format + +```json +{ + "detail": "Human-readable error message" +} +``` + +### Retry Recommendations + +- **400 errors:** Fix request parameters, don't retry +- **404 errors:** Verify job ID, don't retry +- **500 errors:** Check server logs, investigate before retrying +- **503 errors:** Retry with exponential backoff (wait 1s, 2s, 4s, etc.) + +--- + +## Rate Limits and Constraints + +### Concurrency + +- **Maximum concurrent jobs:** 1 (configurable via `MAX_CONCURRENT_JOBS`) +- **Attempting to start a second job returns:** 400 Bad Request + +### Date Range Limits + +- **Maximum date range:** 30 days (configurable via `MAX_SIMULATION_DAYS`) +- **Attempting longer range returns:** 400 Bad Request + +### Price Data + +- **Alpha Vantage API rate limit:** 5 requests/minute (free tier), 75 requests/minute (premium) +- **Automatic download:** Enabled by default (`AUTO_DOWNLOAD_PRICE_DATA=true`) +- **Behavior when rate limited:** Partial data downloaded, simulation continues with available dates + +--- + +## Data Persistence + +All simulation data is stored in SQLite database at `data/jobs.db`. + +### Database Tables + +- **jobs** - Job metadata and status +- **job_details** - Per model-day execution details +- **positions** - Trading position records +- **holdings** - Portfolio holdings breakdown +- **reasoning_logs** - AI decision reasoning (if enabled) +- **tool_usage** - MCP tool usage statistics +- **price_data** - Historical price data cache +- **price_coverage** - Data availability tracking + +### Data Retention + +- Job data persists indefinitely by default +- Results can be queried at any time after job completion +- Manual cleanup: Delete rows from `jobs` table (cascades to related tables) + +--- + +## Configuration + +API behavior is controlled via environment variables and server configuration file. + +### Environment Variables + +See [docs/reference/environment-variables.md](docs/reference/environment-variables.md) for complete reference. + +**Key variables:** + +- `API_PORT` - API server port (default: 8080) +- `MAX_CONCURRENT_JOBS` - Maximum concurrent simulations (default: 1) +- `MAX_SIMULATION_DAYS` - Maximum date range (default: 30) +- `AUTO_DOWNLOAD_PRICE_DATA` - Auto-download missing data (default: true) +- `ALPHAADVANTAGE_API_KEY` - Alpha Vantage API key (required) + +### Server Configuration File + +Server loads model definitions from configuration file (default: `configs/default_config.json`). + +**Example config:** +```json +{ + "models": [ + { + "name": "GPT-4", + "basemodel": "openai/gpt-4", + "signature": "gpt-4", + "enabled": true + }, + { + "name": "Claude 3.7 Sonnet", + "basemodel": "anthropic/claude-3.7-sonnet", + "signature": "claude-3.7-sonnet", + "enabled": true + } + ], + "agent_config": { + "max_steps": 30, + "initial_cash": 10000.0 + } +} +``` + +**Model fields:** + +- `signature` - Unique identifier used in API requests +- `enabled` - Whether model runs when no models specified in request +- `basemodel` - Model identifier for AI provider +- `openai_base_url` - Optional custom API endpoint +- `openai_api_key` - Optional model-specific API key + +--- + +## OpenAPI / Swagger Documentation + +Interactive API documentation available at: + +- Swagger UI: `http://localhost:8080/docs` +- ReDoc: `http://localhost:8080/redoc` +- OpenAPI JSON: `http://localhost:8080/openapi.json` + +--- + +## Client Libraries + +### Python + +```python +import requests +import time + +class AITraderClient: + def __init__(self, base_url="http://localhost:8080"): + self.base_url = base_url + + def trigger_simulation(self, start_date, end_date=None, models=None): + """Trigger a simulation job.""" + payload = {"start_date": start_date} + if end_date: + payload["end_date"] = end_date + if models: + payload["models"] = models + + response = requests.post( + f"{self.base_url}/simulate/trigger", + json=payload + ) + response.raise_for_status() + return response.json() + + def get_status(self, job_id): + """Get job status.""" + response = requests.get(f"{self.base_url}/simulate/status/{job_id}") + response.raise_for_status() + return response.json() + + def wait_for_completion(self, job_id, poll_interval=10): + """Poll until job completes.""" + while True: + status = self.get_status(job_id) + if status["status"] in ["completed", "partial", "failed"]: + return status + time.sleep(poll_interval) + + def get_results(self, job_id=None, date=None, model=None): + """Query results with optional filters.""" + params = {} + if job_id: + params["job_id"] = job_id + if date: + params["date"] = date + if model: + params["model"] = model + + response = requests.get(f"{self.base_url}/results", params=params) + response.raise_for_status() + return response.json() + +# Usage +client = AITraderClient() +job = client.trigger_simulation("2025-01-16", models=["gpt-4"]) +result = client.wait_for_completion(job["job_id"]) +results = client.get_results(job_id=job["job_id"]) +``` + +### TypeScript/JavaScript + +```typescript +class AITraderClient { + constructor(private baseUrl: string = "http://localhost:8080") {} + + async triggerSimulation( + startDate: string, + endDate?: string, + models?: string[] + ) { + const body: any = { start_date: startDate }; + if (endDate) body.end_date = endDate; + if (models) body.models = models; + + const response = await fetch(`${this.baseUrl}/simulate/trigger`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body) + }); + + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + } + + async getStatus(jobId: string) { + const response = await fetch( + `${this.baseUrl}/simulate/status/${jobId}` + ); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + } + + async waitForCompletion(jobId: string, pollInterval: number = 10000) { + while (true) { + const status = await this.getStatus(jobId); + if (["completed", "partial", "failed"].includes(status.status)) { + return status; + } + await new Promise(resolve => setTimeout(resolve, pollInterval)); + } + } + + async getResults(filters: { + jobId?: string; + date?: string; + model?: string; + } = {}) { + const params = new URLSearchParams(); + if (filters.jobId) params.set("job_id", filters.jobId); + if (filters.date) params.set("date", filters.date); + if (filters.model) params.set("model", filters.model); + + const response = await fetch( + `${this.baseUrl}/results?${params.toString()}` + ); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + } +} + +// Usage +const client = new AITraderClient(); +const job = await client.triggerSimulation("2025-01-16", null, ["gpt-4"]); +const result = await client.waitForCompletion(job.job_id); +const results = await client.getResults({ jobId: job.job_id }); +``` diff --git a/CLAUDE.md b/CLAUDE.md index 73d1caa..0610ac5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -303,6 +303,48 @@ When modifying agent behavior or adding tools: 4. Verify position updates in `position/position.jsonl` 5. Use `main.sh` only for full end-to-end testing +See [docs/developer/testing.md](docs/developer/testing.md) for complete testing guide. + +## Documentation Structure + +The project uses a well-organized documentation structure: + +### Root Level (User-facing) +- **README.md** - Project overview, quick start, API overview +- **QUICK_START.md** - 5-minute getting started guide +- **API_REFERENCE.md** - Complete API endpoint documentation +- **CHANGELOG.md** - Release notes and version history +- **TESTING_GUIDE.md** - Testing and validation procedures + +### docs/user-guide/ +- `configuration.md` - Environment setup and model configuration +- `using-the-api.md` - Common workflows and best practices +- `integration-examples.md` - Python, TypeScript, automation examples +- `troubleshooting.md` - Common issues and solutions + +### docs/developer/ +- `CONTRIBUTING.md` - Contribution guidelines +- `development-setup.md` - Local development without Docker +- `testing.md` - Running tests and validation +- `architecture.md` - System design and components +- `database-schema.md` - SQLite table reference +- `adding-models.md` - How to add custom AI models + +### docs/deployment/ +- `docker-deployment.md` - Production Docker setup +- `production-checklist.md` - Pre-deployment verification +- `monitoring.md` - Health checks, logging, metrics +- `scaling.md` - Multiple instances and load balancing + +### docs/reference/ +- `environment-variables.md` - Configuration reference +- `mcp-tools.md` - Trading tool documentation +- `data-formats.md` - File formats and schemas + +### docs/ (Maintainer docs) +- `DOCKER.md` - Docker deployment details +- `RELEASING.md` - Release process for maintainers + ## Common Issues **MCP Services Not Running:** diff --git a/Communication.md b/Communication.md deleted file mode 100644 index 00b5c7e..0000000 --- a/Communication.md +++ /dev/null @@ -1,6 +0,0 @@ -We provide QR codes for joining the HKUDS discussion groups on WeChat and Feishu. - -You can join by scanning the QR codes below: - -WeChat QR Code - diff --git a/DOCKER_API.md b/DOCKER_API.md deleted file mode 100644 index e8299ff..0000000 --- a/DOCKER_API.md +++ /dev/null @@ -1,347 +0,0 @@ -# Docker API Server Deployment - -This guide explains how to run AI-Trader as a persistent REST API server using Docker for Windmill.dev integration. - -## Quick Start - -### 1. Environment Setup - -```bash -# Copy environment template -cp .env.example .env - -# Edit .env and add your API keys: -# - OPENAI_API_KEY -# - ALPHAADVANTAGE_API_KEY -# - JINA_API_KEY -``` - -### 2. Start API Server - -```bash -# Start in API mode (default) -docker-compose up -d ai-trader-api - -# View logs -docker-compose logs -f ai-trader-api - -# Check health -curl http://localhost:8080/health -``` - -### 3. Test API Endpoints - -```bash -# Health check -curl http://localhost:8080/health - -# Trigger simulation -curl -X POST http://localhost:8080/simulate/trigger \ - -H "Content-Type: application/json" \ - -d '{ - "config_path": "/app/configs/default_config.json", - "date_range": ["2025-01-16", "2025-01-17"], - "models": ["gpt-4"] - }' - -# Check job status (replace JOB_ID) -curl http://localhost:8080/simulate/status/JOB_ID - -# Query results -curl http://localhost:8080/results?date=2025-01-16 -``` - -## Architecture - -### Two Deployment Modes - -**API Server Mode** (Windmill integration): -- REST API on port 8080 -- Background job execution -- Persistent SQLite database -- Continuous uptime with health checks -- Start with: `docker-compose up -d ai-trader-api` - -**Batch Mode** (one-time simulation): -- Command-line execution -- Runs to completion then exits -- Config file driven -- Start with: `docker-compose --profile batch up ai-trader-batch` - -### Port Configuration - -| Service | Internal Port | Default Host Port | Environment Variable | -|---------|--------------|-------------------|---------------------| -| API Server | 8080 | 8080 | `API_PORT` | -| Math MCP | 8000 | 8000 | `MATH_HTTP_PORT` | -| Search MCP | 8001 | 8001 | `SEARCH_HTTP_PORT` | -| Trade MCP | 8002 | 8002 | `TRADE_HTTP_PORT` | -| Price MCP | 8003 | 8003 | `GETPRICE_HTTP_PORT` | -| Web Dashboard | 8888 | 8888 | `WEB_HTTP_PORT` | - -## API Endpoints - -### POST /simulate/trigger -Trigger a new simulation job. - -**Request:** -```json -{ - "config_path": "/app/configs/default_config.json", - "date_range": ["2025-01-16", "2025-01-17"], - "models": ["gpt-4", "claude-3.7-sonnet"] -} -``` - -**Response:** -```json -{ - "job_id": "550e8400-e29b-41d4-a716-446655440000", - "status": "pending", - "total_model_days": 4, - "message": "Simulation job created and started" -} -``` - -### GET /simulate/status/{job_id} -Get job progress and status. - -**Response:** -```json -{ - "job_id": "550e8400-e29b-41d4-a716-446655440000", - "status": "running", - "progress": { - "total_model_days": 4, - "completed": 2, - "failed": 0, - "pending": 2 - }, - "date_range": ["2025-01-16", "2025-01-17"], - "models": ["gpt-4", "claude-3.7-sonnet"], - "created_at": "2025-01-16T10:00:00Z", - "details": [ - { - "date": "2025-01-16", - "model": "gpt-4", - "status": "completed", - "started_at": "2025-01-16T10:00:05Z", - "completed_at": "2025-01-16T10:05:23Z", - "duration_seconds": 318.5 - } - ] -} -``` - -### GET /results -Query simulation results with optional filters. - -**Parameters:** -- `job_id` (optional): Filter by job UUID -- `date` (optional): Filter by trading date (YYYY-MM-DD) -- `model` (optional): Filter by model signature - -**Response:** -```json -{ - "results": [ - { - "id": 1, - "job_id": "550e8400-e29b-41d4-a716-446655440000", - "date": "2025-01-16", - "model": "gpt-4", - "action_id": 1, - "action_type": "buy", - "symbol": "AAPL", - "amount": 10, - "price": 250.50, - "cash": 7495.00, - "portfolio_value": 10000.00, - "daily_profit": 0.00, - "daily_return_pct": 0.00, - "holdings": [ - {"symbol": "AAPL", "quantity": 10}, - {"symbol": "CASH", "quantity": 7495.00} - ] - } - ], - "count": 1 -} -``` - -### GET /health -Service health check. - -**Response:** -```json -{ - "status": "healthy", - "database": "connected", - "timestamp": "2025-01-16T10:00:00Z" -} -``` - -## Volume Mounts - -Data persists across container restarts via volume mounts: - -```yaml -volumes: - - ./data:/app/data # SQLite database, price data - - ./logs:/app/logs # Application logs - - ./configs:/app/configs # Configuration files -``` - -**Key files:** -- `/app/data/jobs.db` - SQLite database with job history and results -- `/app/data/merged.jsonl` - Cached price data (fetched on first run) -- `/app/logs/` - Application and MCP service logs - -## Configuration - -### Custom Config File - -Place config files in `./configs/` directory: - -```json -{ - "agent_type": "BaseAgent", - "date_range": { - "init_date": "2025-01-01", - "end_date": "2025-01-31" - }, - "models": [ - { - "name": "GPT-4", - "basemodel": "gpt-4", - "signature": "gpt-4", - "enabled": true - } - ], - "agent_config": { - "max_steps": 30, - "initial_cash": 10000.0 - } -} -``` - -Reference in API calls: `/app/configs/your_config.json` - -## Troubleshooting - -### Check Container Status -```bash -docker-compose ps -docker-compose logs ai-trader-api -``` - -### Health Check Failing -```bash -# Check if services started -docker exec ai-trader-api ps aux - -# Test internal health -docker exec ai-trader-api curl http://localhost:8080/health - -# Check MCP services -docker exec ai-trader-api curl http://localhost:8000/health -``` - -### Database Issues -```bash -# View database -docker exec ai-trader-api sqlite3 data/jobs.db ".tables" - -# Reset database (WARNING: deletes all data) -rm ./data/jobs.db -docker-compose restart ai-trader-api -``` - -### Port Conflicts -If ports are already in use, edit `.env`: -```bash -API_PORT=9080 # Change to available port -``` - -## Windmill Integration - -Example Windmill workflow step: - -```python -import httpx - -def trigger_simulation( - api_url: str, - config_path: str, - start_date: str, - end_date: str, - models: list[str] -): - """Trigger AI trading simulation via API.""" - - response = httpx.post( - f"{api_url}/simulate/trigger", - json={ - "config_path": config_path, - "date_range": [start_date, end_date], - "models": models - }, - timeout=30.0 - ) - - response.raise_for_status() - return response.json() - -def check_status(api_url: str, job_id: str): - """Check simulation job status.""" - - response = httpx.get( - f"{api_url}/simulate/status/{job_id}", - timeout=10.0 - ) - - response.raise_for_status() - return response.json() -``` - -## Production Deployment - -### Use Docker Hub Image -```yaml -# docker-compose.yml -services: - ai-trader-api: - image: ghcr.io/xe138/ai-trader:latest - # ... rest of config -``` - -### Build Locally -```yaml -# docker-compose.yml -services: - ai-trader-api: - build: . - # ... rest of config -``` - -### Environment Security -- Never commit `.env` to version control -- Use secrets management in production (Docker secrets, Kubernetes secrets, etc.) -- Rotate API keys regularly - -## Monitoring - -### Prometheus Metrics (Future) -Metrics endpoint planned: `GET /metrics` - -### Log Aggregation -- Container logs: `docker-compose logs -f` -- Application logs: `./logs/api.log` -- MCP service logs: `./logs/mcp_*.log` - -## Scaling Considerations - -- Single-job concurrency enforced by database lock -- For parallel simulations, deploy multiple instances with separate databases -- Consider load balancer for high-availability setup -- Database size grows with number of simulations (plan for cleanup/archival) diff --git a/QUICK_START.md b/QUICK_START.md new file mode 100644 index 0000000..f82cbcb --- /dev/null +++ b/QUICK_START.md @@ -0,0 +1,373 @@ +# Quick Start Guide + +Get AI-Trader running in under 5 minutes using Docker. + +--- + +## Prerequisites + +- **Docker** and **Docker Compose** installed + - [Install Docker Desktop](https://www.docker.com/products/docker-desktop/) (includes both) +- **API Keys:** + - OpenAI API key ([get one here](https://platform.openai.com/api-keys)) + - Alpha Vantage API key ([free tier](https://www.alphavantage.co/support/#api-key)) + - Jina AI API key ([free tier](https://jina.ai/)) +- **System Requirements:** + - 2GB free disk space + - Internet connection + +--- + +## Step 1: Clone Repository + +```bash +git clone https://github.com/Xe138/AI-Trader.git +cd AI-Trader +``` + +--- + +## Step 2: Configure Environment + +Create `.env` file with your API keys: + +```bash +cp .env.example .env +``` + +Edit `.env` and add your keys: + +```bash +# Required API Keys +OPENAI_API_KEY=sk-your-openai-key-here +ALPHAADVANTAGE_API_KEY=your-alpha-vantage-key-here +JINA_API_KEY=your-jina-key-here + +# Optional: Custom OpenAI endpoint +# OPENAI_API_BASE=https://api.openai.com/v1 + +# Optional: API server port (default: 8080) +# API_PORT=8080 +``` + +**Save the file.** + +--- + +## Step 3: Start the API Server + +```bash +docker-compose up -d +``` + +This will: +- Build the Docker image (~5-10 minutes first time) +- Start the AI-Trader API service +- Start internal MCP services (math, search, trade, price) +- Initialize the SQLite database + +**Wait for startup:** + +```bash +# View logs +docker logs -f ai-trader + +# Wait for this message: +# "Application startup complete" +# Press Ctrl+C to stop viewing logs +``` + +--- + +## Step 4: Verify Service is Running + +```bash +curl http://localhost:8080/health +``` + +**Expected response:** + +```json +{ + "status": "healthy", + "database": "connected", + "timestamp": "2025-01-16T10:00:00Z" +} +``` + +If you see `"status": "healthy"`, you're ready! + +--- + +## Step 5: Run Your First Simulation + +Trigger a simulation for a single day with GPT-4: + +```bash +curl -X POST http://localhost:8080/simulate/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "start_date": "2025-01-16", + "models": ["gpt-4"] + }' +``` + +**Response:** + +```json +{ + "job_id": "550e8400-e29b-41d4-a716-446655440000", + "status": "pending", + "total_model_days": 1, + "message": "Simulation job created with 1 trading dates" +} +``` + +**Save the `job_id`** - you'll need it to check status. + +--- + +## Step 6: Monitor Progress + +```bash +# Replace with your job_id from Step 5 +JOB_ID="550e8400-e29b-41d4-a716-446655440000" + +curl http://localhost:8080/simulate/status/$JOB_ID +``` + +**While running:** + +```json +{ + "job_id": "550e8400-...", + "status": "running", + "progress": { + "total_model_days": 1, + "completed": 0, + "failed": 0, + "pending": 1 + }, + ... +} +``` + +**When complete:** + +```json +{ + "job_id": "550e8400-...", + "status": "completed", + "progress": { + "total_model_days": 1, + "completed": 1, + "failed": 0, + "pending": 0 + }, + ... +} +``` + +**Typical execution time:** 2-5 minutes for a single model-day. + +--- + +## Step 7: View Results + +```bash +curl "http://localhost:8080/results?job_id=$JOB_ID" | jq '.' +``` + +**Example output:** + +```json +{ + "results": [ + { + "id": 1, + "job_id": "550e8400-...", + "date": "2025-01-16", + "model": "gpt-4", + "action_type": "buy", + "symbol": "AAPL", + "amount": 10, + "price": 250.50, + "cash": 7495.00, + "portfolio_value": 10000.00, + "daily_profit": 0.00, + "holdings": [ + {"symbol": "AAPL", "quantity": 10}, + {"symbol": "CASH", "quantity": 7495.00} + ] + } + ], + "count": 1 +} +``` + +You can see: +- What the AI decided to buy/sell +- Portfolio value and cash balance +- All current holdings + +--- + +## Success! What's Next? + +### Run Multiple Days + +```bash +curl -X POST http://localhost:8080/simulate/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "start_date": "2025-01-16", + "end_date": "2025-01-20" + }' +``` + +This simulates 5 trading days (weekdays only). + +### Run Multiple Models + +```bash +curl -X POST http://localhost:8080/simulate/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "start_date": "2025-01-16", + "models": ["gpt-4", "claude-3.7-sonnet"] + }' +``` + +**Note:** Models must be defined and enabled in `configs/default_config.json`. + +### Query Specific Results + +```bash +# All results for a specific date +curl "http://localhost:8080/results?date=2025-01-16" + +# All results for a specific model +curl "http://localhost:8080/results?model=gpt-4" + +# Combine filters +curl "http://localhost:8080/results?date=2025-01-16&model=gpt-4" +``` + +--- + +## Troubleshooting + +### Service won't start + +```bash +# Check logs +docker logs ai-trader + +# Common issues: +# - Missing API keys in .env +# - Port 8080 already in use +# - Docker not running +``` + +**Fix port conflicts:** + +Edit `.env` and change `API_PORT`: + +```bash +API_PORT=8889 +``` + +Then restart: + +```bash +docker-compose down +docker-compose up -d +``` + +### Health check returns error + +```bash +# Check if container is running +docker ps | grep ai-trader + +# Restart service +docker-compose restart + +# Check for errors in logs +docker logs ai-trader | grep -i error +``` + +### Job stays "pending" + +The simulation might still be downloading price data on first run. + +```bash +# Watch logs in real-time +docker logs -f ai-trader + +# Look for messages like: +# "Downloading missing price data..." +# "Starting simulation for model-day..." +``` + +First run can take 10-15 minutes while downloading historical price data. + +### "No trading dates with complete price data" + +This means price data is missing for the requested date range. + +**Solution 1:** Try a different date range (recent dates work best) + +**Solution 2:** Manually download price data: + +```bash +docker exec -it ai-trader bash +cd data +python get_daily_price.py +python merge_jsonl.py +exit +``` + +--- + +## Common Commands + +```bash +# View logs +docker logs -f ai-trader + +# Stop service +docker-compose down + +# Start service +docker-compose up -d + +# Restart service +docker-compose restart + +# Check health +curl http://localhost:8080/health + +# Access container shell +docker exec -it ai-trader bash + +# View database +docker exec -it ai-trader sqlite3 /app/data/jobs.db +``` + +--- + +## Next Steps + +- **Full API Reference:** [API_REFERENCE.md](API_REFERENCE.md) +- **Configuration Guide:** [docs/user-guide/configuration.md](docs/user-guide/configuration.md) +- **Integration Examples:** [docs/user-guide/integration-examples.md](docs/user-guide/integration-examples.md) +- **Troubleshooting:** [docs/user-guide/troubleshooting.md](docs/user-guide/troubleshooting.md) + +--- + +## Need Help? + +- Check [docs/user-guide/troubleshooting.md](docs/user-guide/troubleshooting.md) +- Review logs: `docker logs ai-trader` +- Open an issue: [GitHub Issues](https://github.com/Xe138/AI-Trader/issues) diff --git a/README.md b/README.md index 8f68e4c..000bdab 100644 --- a/README.md +++ b/README.md @@ -9,46 +9,17 @@ **REST API service for autonomous AI trading competitions. Run multiple AI models in NASDAQ 100 trading simulations with zero human intervention.** -[๐Ÿš€ Quick Start](#-quick-start) โ€ข [๐Ÿ“š API Documentation](#-api-documentation) โ€ข [๐Ÿณ Docker Deployment](#-docker-deployment) โ€ข [ไธญๆ–‡ๆ–‡ๆกฃ](README_CN.md) +[๐Ÿš€ Quick Start](QUICK_START.md) โ€ข [๐Ÿ“š API Reference](API_REFERENCE.md) โ€ข [๐Ÿ“– Documentation](#documentation) โ€ข [ไธญๆ–‡ๆ–‡ๆกฃ](README_CN.md) --- -## โœจ Latest Updates (v0.3.0) - -**Major Architecture Upgrade - REST API Service** - -- ๐ŸŒ **REST API Server** - Complete FastAPI implementation - - `POST /simulate/trigger` - Start simulation jobs with date ranges - - `GET /simulate/status/{job_id}` - Monitor progress in real-time - - `GET /results` - Query results with filtering - - `GET /health` - Service health checks -- ๐Ÿ’พ **SQLite Database** - Complete persistence layer - - Price data storage with on-demand downloads - - Job tracking and lifecycle management - - Position records with P&L tracking - - AI reasoning logs and tool usage analytics -- ๐Ÿ“Š **On-Demand Price Data** - Automatic gap filling - - Priority-based download strategy - - Graceful rate limit handling - - Coverage tracking per symbol -- ๐Ÿณ **Production-Ready Docker** - Single-command deployment - - Health checks and automatic restarts - - Volume persistence for data and logs - - Simplified configuration -- ๐Ÿงช **Comprehensive Testing** - 175 tests with high coverage -- ๐Ÿ“š **Complete Documentation** - API guides and validation procedures - -See [CHANGELOG.md](CHANGELOG.md) for full release notes and [ROADMAP.md](ROADMAP.md) for planned features. - ---- - ## ๐ŸŒŸ What is AI-Trader? > **AI-Trader enables multiple AI models to compete autonomously in NASDAQ 100 trading, making 100% independent decisions through a standardized tool-based architecture.** -### ๐ŸŽฏ Core Features +### Key Features - ๐Ÿค– **Fully Autonomous Trading** - AI agents analyze, decide, and execute without human intervention - ๐ŸŒ **REST API Architecture** - Trigger simulations and monitor results via HTTP @@ -57,7 +28,7 @@ See [CHANGELOG.md](CHANGELOG.md) for full release notes and [ROADMAP.md](ROADMAP - ๐Ÿ“Š **Real-Time Analytics** - Track positions, P&L, and AI decision reasoning - โฐ **Historical Replay** - Backtest with anti-look-ahead controls - ๐Ÿ’พ **Persistent Storage** - SQLite database for all results and analytics -- ๐Ÿ”Œ **External Orchestration** - Integrate with Windmill.dev or any HTTP client +- ๐Ÿ”Œ **External Orchestration** - Integrate with any HTTP client or workflow automation service --- @@ -94,20 +65,11 @@ See [CHANGELOG.md](CHANGELOG.md) for full release notes and [ROADMAP.md](ROADMAP โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` -### Key Components - -- **FastAPI Server** - RESTful interface for job management and results -- **Job Manager** - Coordinates simulation execution, prevents concurrent jobs -- **Simulation Worker** - Orchestrates date-sequential, model-parallel execution -- **Model-Day Executor** - Runs single model for single date with isolated config -- **SQLite Database** - Persistent storage with 6 relational tables -- **MCP Services** - Internal tool ecosystem (math, search, trade, price) - --- ## ๐Ÿš€ Quick Start -### ๐Ÿณ Docker Deployment (Recommended) +### Docker Deployment (5 minutes) **1. Prerequisites** - Docker and Docker Compose installed @@ -115,369 +77,242 @@ See [CHANGELOG.md](CHANGELOG.md) for full release notes and [ROADMAP.md](ROADMAP **2. Setup** ```bash -# Clone repository git clone https://github.com/Xe138/AI-Trader.git cd AI-Trader # Configure environment cp .env.example .env -# Edit .env and add your API keys: -# OPENAI_API_KEY=your_key_here -# ALPHAADVANTAGE_API_KEY=your_key_here -# JINA_API_KEY=your_key_here +# Edit .env and add your API keys ``` -**3. Start API Server** +**3. Start Service** ```bash -# Start in background docker-compose up -d -# View logs -docker logs -f ai-trader - # Verify health curl http://localhost:8080/health ``` -**4. Trigger Simulation** +**4. Run Simulation** ```bash curl -X POST http://localhost:8080/simulate/trigger \ -H "Content-Type: application/json" \ -d '{ "start_date": "2025-01-16", - "end_date": "2025-01-17", "models": ["gpt-4"] }' ``` **5. Monitor Progress** ```bash -# Get job status (use job_id from trigger response) +# Use job_id from trigger response curl http://localhost:8080/simulate/status/{job_id} - -# View results -curl http://localhost:8080/results?job_id={job_id} ``` +**6. View Results** +```bash +curl "http://localhost:8080/results?job_id={job_id}" +``` + +๐Ÿ“– **Detailed guide:** [QUICK_START.md](QUICK_START.md) + --- -## ๐Ÿ“š API Documentation +## ๐Ÿ“š API Overview ### Endpoints -#### `POST /simulate/trigger` -Start a new simulation job. +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/simulate/trigger` | POST | Start simulation job | +| `/simulate/status/{job_id}` | GET | Check job progress | +| `/results` | GET | Query trading results | +| `/health` | GET | Service health check | + +### Example: Trigger Simulation **Request:** -```json -{ - "start_date": "2025-01-16", - "end_date": "2025-01-17", - "models": ["gpt-4", "claude-3.7-sonnet"] -} +```bash +curl -X POST http://localhost:8080/simulate/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "start_date": "2025-01-16", + "end_date": "2025-01-17", + "models": ["gpt-4", "claude-3.7-sonnet"] + }' ``` -**Parameters:** -- `start_date` (required) - Start date in YYYY-MM-DD format -- `end_date` (optional) - End date in YYYY-MM-DD format. If omitted, defaults to `start_date` (single day) -- `models` (optional) - Array of model signatures to run. If omitted, runs all enabled models from config - **Response:** ```json { "job_id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending", "total_model_days": 4, - "message": "Simulation job created and started" + "message": "Simulation job created with 2 trading dates" } ``` -#### `GET /simulate/status/{job_id}` -Query job execution status and progress. +**Parameters:** +- `start_date` (required) - Start date in YYYY-MM-DD format +- `end_date` (optional) - End date, defaults to `start_date` for single-day simulation +- `models` (optional) - Model signatures to run, defaults to all enabled models in config -**Response:** -```json -{ - "job_id": "550e8400-...", - "status": "running", - "progress": { - "completed": 2, - "failed": 0, - "pending": 2, - "total": 4 - }, - "details": [ - { - "model_signature": "gpt-4", - "trading_date": "2025-01-16", - "status": "completed", - "start_time": "2025-01-16T10:00:00", - "end_time": "2025-01-16T10:05:00" +๐Ÿ“– **Complete reference:** [API_REFERENCE.md](API_REFERENCE.md) + +--- + +## ๐ŸŽฏ Trading Environment + +- ๐Ÿ’ฐ **Initial Capital**: $10,000 per AI model +- ๐Ÿ“ˆ **Trading Universe**: NASDAQ 100 stocks +- โฐ **Trading Schedule**: Weekdays only (historical simulation) +- ๐Ÿ“Š **Data Sources**: Alpha Vantage (prices) + Jina AI (market intelligence) +- ๐Ÿ”„ **Anti-Look-Ahead**: Data access limited to current date and earlier + +--- + +## ๐Ÿง  AI Agent Capabilities + +Through the MCP (Model Context Protocol) toolchain, AI agents can: + +- ๐Ÿ“ฐ **Research Markets** - Search news, analyst reports, financial data +- ๐Ÿ“Š **Query Prices** - Get real-time and historical OHLCV data +- ๐Ÿ’ฐ **Execute Trades** - Buy/sell stocks, manage positions +- ๐Ÿงฎ **Perform Calculations** - Mathematical analysis and computations +- ๐Ÿ“ **Log Reasoning** - Document decision-making process + +**All operations are 100% autonomous - zero human intervention or pre-programmed strategies.** + +--- + +## ๐Ÿ”Œ Integration Examples + +### Python Client + +```python +import requests +import time + +class AITraderClient: + def __init__(self, base_url="http://localhost:8080"): + self.base_url = base_url + + def trigger_simulation(self, start_date, end_date=None, models=None): + payload = {"start_date": start_date} + if end_date: + payload["end_date"] = end_date + if models: + payload["models"] = models + + response = requests.post( + f"{self.base_url}/simulate/trigger", + json=payload + ) + response.raise_for_status() + return response.json() + + def wait_for_completion(self, job_id, poll_interval=10): + while True: + response = requests.get( + f"{self.base_url}/simulate/status/{job_id}" + ) + status = response.json() + + if status["status"] in ["completed", "partial", "failed"]: + return status + + time.sleep(poll_interval) + +# Usage +client = AITraderClient() +job = client.trigger_simulation("2025-01-16", models=["gpt-4"]) +result = client.wait_for_completion(job["job_id"]) +``` + +### TypeScript/JavaScript + +```typescript +async function runSimulation() { + // Trigger simulation + const response = await fetch("http://localhost:8080/simulate/trigger", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + start_date: "2025-01-16", + models: ["gpt-4"] + }) + }); + + const job = await response.json(); + + // Poll for completion + while (true) { + const statusResponse = await fetch( + `http://localhost:8080/simulate/status/${job.job_id}` + ); + const status = await statusResponse.json(); + + if (["completed", "partial", "failed"].includes(status.status)) { + return status; } - ] + + await new Promise(resolve => setTimeout(resolve, 10000)); + } } ``` -#### `GET /results` -Retrieve simulation results with optional filtering. +### Scheduled Automation -**Query Parameters:** -- `job_id` - Filter by job UUID -- `date` - Filter by trading date (YYYY-MM-DD) -- `model` - Filter by model signature - -**Response:** -```json -{ - "count": 2, - "results": [ - { - "job_id": "550e8400-...", - "model_signature": "gpt-4", - "trading_date": "2025-01-16", - "final_cash": 9850.50, - "total_value": 10250.75, - "profit_loss": 250.75, - "positions": {...}, - "holdings": [...] - } - ] -} -``` - -#### `GET /health` -Service health check. - -**Response:** -```json -{ - "status": "healthy", - "database": "connected", - "timestamp": "2025-01-16T10:00:00Z" -} -``` - -### Complete API Reference - -#### Request Validation Rules - -**Date Format:** -- Must be YYYY-MM-DD format -- Must be valid calendar date -- Cannot be in the future -- `start_date` must be <= `end_date` -- Maximum date range: 30 days (configurable via `MAX_SIMULATION_DAYS`) - -**Model Selection:** -- Must match signatures defined in server configuration file -- Only enabled models will run -- If `models` array omitted, all enabled models from server config run - -**Server Configuration:** -- Config file path is set when starting the API server (not per-request) -- Default: `configs/default_config.json` -- Set via environment variable: `CONFIG_PATH=/path/to/config.json` -- Contains model definitions, agent settings, and defaults - -#### Error Responses - -All API endpoints return consistent error responses: - -```json -{ - "detail": "Error message describing what went wrong" -} -``` - -**Common HTTP Status Codes:** -- `200 OK` - Successful request -- `400 Bad Request` - Invalid parameters or validation failure -- `404 Not Found` - Job ID not found -- `409 Conflict` - Job already running (concurrent job prevention) -- `500 Internal Server Error` - Unexpected server error - -**Example Validation Errors:** - -Invalid date format: -```json -{ - "detail": "Invalid date format: 2025-1-16. Use YYYY-MM-DD" -} -``` - -Date range too large: -```json -{ - "detail": "Date range too large: 45 days. Maximum allowed: 30 days" -} -``` - -Future date: -```json -{ - "detail": "Dates cannot be in the future: 2026-01-16" -} -``` - -Concurrent job: -```json -{ - "detail": "Another simulation job is already running: " -} -``` - -#### Job Status Values - -- `pending` - Job created, waiting to start -- `running` - Job currently executing -- `completed` - All model-days completed successfully -- `partial` - Some model-days completed, some failed -- `failed` - All model-days failed - -#### Model-Day Status Values - -- `pending` - Not started yet -- `running` - Currently executing -- `completed` - Finished successfully -- `failed` - Execution failed with error - -### Advanced API Usage - -#### On-Demand Price Data Downloads - -AI-Trader automatically downloads missing price data when needed: - -1. **Automatic Gap Detection** - System checks database for missing date ranges -2. **Priority-Based Downloads** - Downloads symbols that complete the most dates first -3. **Rate Limit Handling** - Gracefully handles Alpha Vantage API limits -4. **Coverage Tracking** - Records downloaded date ranges per symbol - -**Configuration:** -```bash -# Enable/disable automatic downloads (default: true) -AUTO_DOWNLOAD_PRICE_DATA=true - -# Alpha Vantage API key (required for downloads) -ALPHAADVANTAGE_API_KEY=your_key_here -``` - -**Download Behavior:** -- If price data exists in database, uses cached data (no API call) -- If data missing, downloads from Alpha Vantage during simulation -- Rate limit hit: Pauses downloads, continues simulation with available data -- Next simulation resumes downloads where it left off - -**Example Workflow:** -```bash -# First run: Downloads data for requested dates -curl -X POST http://localhost:8080/simulate/trigger \ - -H "Content-Type: application/json" \ - -d '{ - "start_date": "2025-01-16", - "end_date": "2025-01-20", - "models": ["gpt-4"] - }' -# Downloads AAPL, MSFT, GOOGL for 2025-01-16 to 2025-01-20 - -# Second run: Reuses cached data, no downloads -curl -X POST http://localhost:8080/simulate/trigger \ - -H "Content-Type: application/json" \ - -d '{ - "start_date": "2025-01-18", - "end_date": "2025-01-19", - "models": ["gpt-4"] - }' -# Uses cached data, zero API calls - -# Third run: Only downloads new dates -curl -X POST http://localhost:8080/simulate/trigger \ - -H "Content-Type: application/json" \ - -d '{ - "start_date": "2025-01-20", - "end_date": "2025-01-22", - "models": ["gpt-4"] - }' -# Reuses 2025-01-20 data, downloads 2025-01-21 and 2025-01-22 -``` - -#### Detail Levels - -Control how much data is logged during simulation: - -**Summary Mode (default):** -```bash -curl -X POST http://localhost:8080/simulate/trigger \ - -H "Content-Type: application/json" \ - -d '{ - "start_date": "2025-01-16", - "models": ["gpt-4"] - }' -``` -- Logs positions, P&L, and tool usage -- Does NOT log AI reasoning steps -- Minimal database storage -- Faster execution - -**Full Mode:** -```bash -# Note: Detail level control not yet implemented in v0.3.0 -# All simulations currently log complete data -curl -X POST http://localhost:8080/simulate/trigger \ - -H "Content-Type: application/json" \ - -d '{ - "start_date": "2025-01-16", - "models": ["gpt-4"] - }' -``` -- Logs positions, P&L, tool usage, AND AI reasoning -- Stores complete conversation history in `reasoning_logs` table -- Larger database footprint -- Useful for debugging AI decision-making - -**Querying Reasoning Logs:** -```bash -docker exec -it ai-trader sqlite3 /app/data/jobs.db -sqlite> SELECT * FROM reasoning_logs WHERE job_id='...' AND date='2025-01-16'; -``` - -#### Concurrent Job Prevention - -Only one simulation can run at a time: +Use any scheduler (cron, Airflow, etc.): ```bash -# Start first job +#!/bin/bash +# daily_simulation.sh + +DATE=$(date -d "yesterday" +%Y-%m-%d) + curl -X POST http://localhost:8080/simulate/trigger \ -H "Content-Type: application/json" \ - -d '{ - "start_date": "2025-01-16", - "models": ["gpt-4"] - }' -# Response: {"job_id": "abc123", "status": "running"} - -# Try to start second job (will fail) -curl -X POST http://localhost:8080/simulate/trigger \ - -H "Content-Type: application/json" \ - -d '{ - "start_date": "2025-01-17", - "models": ["gpt-4"] - }' -# Response: 409 Conflict -# {"detail": "Another simulation job is already running: abc123"} - -# Wait for first job to complete -curl http://localhost:8080/simulate/status/abc123 -# {"status": "completed", ...} - -# Now second job can start -curl -X POST http://localhost:8080/simulate/trigger \ - -H "Content-Type: application/json" \ - -d '{ - "start_date": "2025-01-17", - "models": ["gpt-4"] - }' -# Response: {"job_id": "def456", "status": "running"} + -d "{\"start_date\": \"$DATE\", \"models\": [\"gpt-4\"]}" ``` +Add to crontab: +``` +0 6 * * * /path/to/daily_simulation.sh +``` + +๐Ÿ“– **More examples:** [docs/user-guide/integration-examples.md](docs/user-guide/integration-examples.md) + +--- + +## ๐Ÿ“– Documentation + +### User Guides +- [Quick Start](QUICK_START.md) - Get running in 5 minutes +- [Configuration Guide](docs/user-guide/configuration.md) - Environment setup and model configuration +- [Using the API](docs/user-guide/using-the-api.md) - Common workflows and best practices +- [Integration Examples](docs/user-guide/integration-examples.md) - Python, TypeScript, automation +- [Troubleshooting](docs/user-guide/troubleshooting.md) - Common issues and solutions + +### Developer Documentation +- [Development Setup](docs/developer/development-setup.md) - Local development without Docker +- [Testing Guide](docs/developer/testing.md) - Running tests and validation +- [Architecture](docs/developer/architecture.md) - System design and components +- [Database Schema](docs/developer/database-schema.md) - SQLite table reference +- [Adding Models](docs/developer/adding-models.md) - How to add custom AI models + +### Deployment +- [Docker Deployment](docs/deployment/docker-deployment.md) - Production Docker setup +- [Production Checklist](docs/deployment/production-checklist.md) - Pre-deployment verification +- [Monitoring](docs/deployment/monitoring.md) - Health checks, logging, metrics +- [Scaling](docs/deployment/scaling.md) - Multiple instances and load balancing + +### Reference +- [API Reference](API_REFERENCE.md) - Complete endpoint documentation +- [Environment Variables](docs/reference/environment-variables.md) - Configuration reference +- [MCP Tools](docs/reference/mcp-tools.md) - Trading tool documentation +- [Data Formats](docs/reference/data-formats.md) - File formats and schemas + --- ## ๐Ÿ› ๏ธ Configuration @@ -485,54 +320,72 @@ curl -X POST http://localhost:8080/simulate/trigger \ ### Environment Variables ```bash -# AI Model API Configuration -OPENAI_API_BASE= # Optional: custom OpenAI proxy -OPENAI_API_KEY=your_key_here # Required: OpenAI API key +# Required API Keys +OPENAI_API_KEY=sk-your-key-here +ALPHAADVANTAGE_API_KEY=your-key-here +JINA_API_KEY=your-key-here -# Data Source Configuration -ALPHAADVANTAGE_API_KEY=your_key_here # Required: Alpha Vantage -JINA_API_KEY=your_key_here # Required: Jina AI search - -# API Server Port (host-side mapping) -API_PORT=8080 # Change if port 8080 is occupied - -# Agent Configuration -AGENT_MAX_STEP=30 # Maximum reasoning steps per day - -# Data Volume Configuration -VOLUME_PATH=. # Base directory for persistent data +# Optional Configuration +API_PORT=8080 # API server port +MAX_CONCURRENT_JOBS=1 # Max simultaneous simulations +MAX_SIMULATION_DAYS=30 # Max date range per job +AUTO_DOWNLOAD_PRICE_DATA=true # Auto-fetch missing data ``` -### Configuration File +### Model Configuration -Create custom configs in `configs/` directory: +Edit `configs/default_config.json`: ```json { - "agent_type": "BaseAgent", - "date_range": { - "init_date": "2025-01-01", - "end_date": "2025-01-31" - }, "models": [ { "name": "GPT-4", "basemodel": "openai/gpt-4", "signature": "gpt-4", "enabled": true + }, + { + "name": "Claude 3.7 Sonnet", + "basemodel": "anthropic/claude-3.7-sonnet", + "signature": "claude-3.7-sonnet", + "enabled": true, + "openai_base_url": "https://api.anthropic.com/v1", + "openai_api_key": "your-anthropic-key" } ], "agent_config": { "max_steps": 30, - "max_retries": 3, "initial_cash": 10000.0 - }, - "log_config": { - "log_path": "./data/agent_data" } } ``` +๐Ÿ“– **Full guide:** [docs/user-guide/configuration.md](docs/user-guide/configuration.md) + +--- + +## ๐Ÿ“Š Database Schema + +SQLite database at `data/jobs.db` contains: + +- **jobs** - Job metadata and status +- **job_details** - Per model-day execution details +- **positions** - Trading position records with P&L +- **holdings** - Portfolio holdings breakdown +- **reasoning_logs** - AI decision reasoning history +- **tool_usage** - MCP tool usage statistics +- **price_data** - Historical price data cache +- **price_coverage** - Data availability tracking + +Query directly: +```bash +docker exec -it ai-trader sqlite3 /app/data/jobs.db +sqlite> SELECT * FROM jobs ORDER BY created_at DESC LIMIT 5; +``` + +๐Ÿ“– **Schema reference:** [docs/developer/database-schema.md](docs/developer/database-schema.md) + --- ## ๐Ÿงช Testing & Validation @@ -550,217 +403,7 @@ bash scripts/validate_docker_build.sh bash scripts/test_api_endpoints.sh ``` -### Manual Testing - -```bash -# 1. Start API server -docker-compose up -d - -# 2. Health check -curl http://localhost:8080/health - -# 3. Trigger small test job (single day) -curl -X POST http://localhost:8080/simulate/trigger \ - -H "Content-Type: application/json" \ - -d '{ - "start_date": "2025-01-16", - "models": ["gpt-4"] - }' - -# 4. Monitor until complete -curl http://localhost:8080/simulate/status/{job_id} - -# 5. View results -curl http://localhost:8080/results?job_id={job_id} -``` - -See [TESTING_GUIDE.md](TESTING_GUIDE.md) for comprehensive testing procedures and troubleshooting. - ---- - -## ๐ŸŽฏ Trading Environment - -- ๐Ÿ’ฐ **Initial Capital**: $10,000 per AI model -- ๐Ÿ“ˆ **Trading Universe**: NASDAQ 100 stocks -- โฐ **Trading Schedule**: Weekdays only (historical simulation) -- ๐Ÿ“Š **Data Sources**: Alpha Vantage (prices) + Jina AI (market intelligence) -- ๐Ÿ”„ **Anti-Look-Ahead**: Data access limited to current date and earlier - ---- - -## ๐Ÿง  AI Agent Capabilities - -Through the MCP (Model Context Protocol) toolchain, AI agents can: - -- ๐Ÿ“ฐ **Research Markets** - Search news, analyst reports, financial data (Jina AI) -- ๐Ÿ“Š **Query Prices** - Get real-time and historical OHLCV data -- ๐Ÿ’ฐ **Execute Trades** - Buy/sell stocks, manage positions -- ๐Ÿงฎ **Perform Calculations** - Mathematical analysis and computations -- ๐Ÿ“ **Log Reasoning** - Document decision-making process - -**All operations are 100% autonomous - zero human intervention or pre-programmed strategies.** - ---- - -## ๐Ÿ“ Project Structure - -``` -AI-Trader/ -โ”œโ”€โ”€ api/ # FastAPI application -โ”‚ โ”œโ”€โ”€ main.py # API server entry point -โ”‚ โ”œโ”€โ”€ database.py # SQLite schema and operations -โ”‚ โ”œโ”€โ”€ job_manager.py # Job lifecycle management -โ”‚ โ”œโ”€โ”€ simulation_worker.py # Job orchestration -โ”‚ โ”œโ”€โ”€ model_day_executor.py # Single model-day execution -โ”‚ โ”œโ”€โ”€ runtime_manager.py # Isolated runtime configs -โ”‚ โ””โ”€โ”€ models.py # Pydantic request/response models -โ”‚ -โ”œโ”€โ”€ agent/ # AI agent core -โ”‚ โ””โ”€โ”€ base_agent/ -โ”‚ โ””โ”€โ”€ base_agent.py # BaseAgent implementation -โ”‚ -โ”œโ”€โ”€ agent_tools/ # MCP service implementations -โ”‚ โ”œโ”€โ”€ tool_math.py # Mathematical calculations -โ”‚ โ”œโ”€โ”€ tool_jina_search.py # Market intelligence search -โ”‚ โ”œโ”€โ”€ tool_trade.py # Trading execution -โ”‚ โ”œโ”€โ”€ tool_get_price_local.py # Price queries -โ”‚ โ””โ”€โ”€ start_mcp_services.py # Service orchestration -โ”‚ -โ”œโ”€โ”€ tests/ # Test suite (102 tests, 85% coverage) -โ”‚ โ”œโ”€โ”€ unit/ # Unit tests -โ”‚ โ””โ”€โ”€ integration/ # Integration tests -โ”‚ -โ”œโ”€โ”€ configs/ # Configuration files -โ”‚ โ””โ”€โ”€ default_config.json # Default simulation config -โ”‚ -โ”œโ”€โ”€ scripts/ # Validation scripts -โ”‚ โ”œโ”€โ”€ validate_docker_build.sh # Docker build validation -โ”‚ โ””โ”€โ”€ test_api_endpoints.sh # API endpoint testing -โ”‚ -โ”œโ”€โ”€ data/ # Persistent data (volume mount) -โ”‚ โ”œโ”€โ”€ jobs.db # SQLite database -โ”‚ โ””โ”€โ”€ agent_data/ # Agent execution data -โ”‚ -โ”œโ”€โ”€ docker-compose.yml # Docker orchestration -โ”œโ”€โ”€ Dockerfile # Container image definition -โ”œโ”€โ”€ entrypoint.sh # Container startup script -โ”œโ”€โ”€ requirements.txt # Python dependencies -โ””โ”€โ”€ README.md # This file -``` - ---- - -## ๐Ÿ”Œ Integration Examples - -### Windmill.dev Workflow - -```typescript -// Trigger simulation -export async function triggerSimulation( - api_url: string, - start_date: string, - end_date: string | null, - models: string[] -) { - const body: any = { start_date, models }; - if (end_date) { - body.end_date = end_date; - } - - const response = await fetch(`${api_url}/simulate/trigger`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body) - }); - return response.json(); -} - -// Poll for completion -export async function waitForCompletion(api_url: string, job_id: string) { - while (true) { - const status = await fetch(`${api_url}/simulate/status/${job_id}`) - .then(r => r.json()); - - if (['completed', 'failed', 'partial'].includes(status.status)) { - return status; - } - - await new Promise(resolve => setTimeout(resolve, 10000)); // 10s poll - } -} - -// Get results -export async function getResults(api_url: string, job_id: string) { - return fetch(`${api_url}/results?job_id=${job_id}`) - .then(r => r.json()); -} -``` - -### Python Client - -```python -import requests -import time - -# Example 1: Trigger simulation with date range -response = requests.post('http://localhost:8080/simulate/trigger', json={ - 'start_date': '2025-01-16', - 'end_date': '2025-01-17', - 'models': ['gpt-4', 'claude-3.7-sonnet'] -}) -job_id = response.json()['job_id'] - -# Example 2: Trigger single day (omit end_date) -response_single = requests.post('http://localhost:8080/simulate/trigger', json={ - 'start_date': '2025-01-16', - 'models': ['gpt-4'] -}) -job_id_single = response_single.json()['job_id'] - -# Poll for completion -while True: - status = requests.get(f'http://localhost:8080/simulate/status/{job_id}').json() - if status['status'] in ['completed', 'failed', 'partial']: - break - time.sleep(10) - -# Get results -results = requests.get(f'http://localhost:8080/results?job_id={job_id}').json() -print(f"Completed with {results['count']} results") -``` - ---- - -## ๐Ÿ“Š Database Schema - -The SQLite database (`data/jobs.db`) contains: - -### Tables - -- **jobs** - Job metadata (id, status, created_at, etc.) -- **job_details** - Per model-day execution details -- **positions** - Trading position records with P&L -- **holdings** - Portfolio holdings breakdown -- **reasoning_logs** - AI decision reasoning history -- **tool_usage** - MCP tool usage statistics - -### Querying Data - -```bash -# Direct database access -docker exec -it ai-trader sqlite3 /app/data/jobs.db - -# Example queries -sqlite> SELECT * FROM jobs ORDER BY created_at DESC LIMIT 5; -sqlite> SELECT model_signature, AVG(profit_loss) FROM positions GROUP BY model_signature; -sqlite> SELECT * FROM reasoning_logs WHERE job_id='...'; -``` - ---- - -## ๐Ÿ› ๏ธ Development - -### Running Tests +### Unit Tests ```bash # Install dependencies @@ -768,44 +411,36 @@ pip install -r requirements.txt # Run test suite pytest tests/ -v --cov=api --cov-report=term-missing - -# Run specific test -pytest tests/unit/test_job_manager.py -v ``` -### Adding Custom Models - -Edit `configs/default_config.json`: - -```json -{ - "models": [ - { - "name": "Custom Model", - "basemodel": "provider/model-name", - "signature": "custom-model", - "enabled": true, - "openai_base_url": "https://api.custom.com/v1", - "openai_api_key": "custom_key_here" - } - ] -} -``` +๐Ÿ“– **Testing guide:** [docs/developer/testing.md](docs/developer/testing.md) --- -## ๐Ÿ“– Documentation +## ๐Ÿ“ˆ Latest Updates -- [CHANGELOG.md](CHANGELOG.md) - Release notes and version history -- [DOCKER_API.md](DOCKER_API.md) - Detailed API deployment guide -- [TESTING_GUIDE.md](TESTING_GUIDE.md) - Comprehensive testing procedures -- [CLAUDE.md](CLAUDE.md) - Development guide for contributors +### v0.3.0 (Current) + +**Major Architecture Upgrade - REST API Service** + +- ๐ŸŒ **REST API Server** - Complete FastAPI implementation + - `POST /simulate/trigger` - Start simulation jobs with date ranges + - `GET /simulate/status/{job_id}` - Monitor progress in real-time + - `GET /results` - Query results with filtering + - `GET /health` - Service health checks +- ๐Ÿ’พ **SQLite Database** - Complete persistence layer +- ๐Ÿ“Š **On-Demand Price Data** - Automatic gap filling with priority-based downloads +- ๐Ÿณ **Production-Ready Docker** - Single-command deployment +- ๐Ÿงช **Comprehensive Testing** - 175 tests with high coverage +- ๐Ÿ“š **Complete Documentation** - API guides and validation procedures + +See [CHANGELOG.md](CHANGELOG.md) for full release notes and [ROADMAP.md](ROADMAP.md) for planned features. --- ## ๐Ÿค Contributing -Contributions welcome! Please read [CLAUDE.md](CLAUDE.md) for development guidelines. +Contributions welcome! Please read [docs/developer/CONTRIBUTING.md](docs/developer/CONTRIBUTING.md) for development guidelines. --- @@ -820,6 +455,7 @@ MIT License - see [LICENSE](LICENSE) for details - **GitHub**: https://github.com/Xe138/AI-Trader - **Docker Hub**: `ghcr.io/xe138/ai-trader:latest` - **Issues**: https://github.com/Xe138/AI-Trader/issues +- **API Docs**: http://localhost:8080/docs (when running) --- @@ -827,4 +463,6 @@ MIT License - see [LICENSE](LICENSE) for details **Built with FastAPI, SQLite, Docker, and the MCP Protocol** +[โฌ† Back to top](#-ai-trader-can-ai-beat-the-market) + diff --git a/ROADMAP.md b/ROADMAP.md index 7b85f35..be51dc4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -66,6 +66,18 @@ This document outlines planned features and improvements for the AI-Trader proje - Chart library (Plotly.js, Chart.js, or Recharts) - Served alongside API (single container deployment) +#### Development Infrastructure +- **Migration to uv Package Manager** - Modern Python package management + - Replace pip with uv for dependency management + - Create pyproject.toml with project metadata and dependencies + - Update Dockerfile to use uv for faster, more reliable builds + - Update development documentation and workflows + - Benefits: + - 10-100x faster dependency resolution and installation + - Better dependency locking and reproducibility + - Unified tool for virtual environments and package management + - Drop-in pip replacement with improved UX + ## Contributing We welcome contributions to any of these planned features! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. diff --git a/docs/ENHANCED-SPECIFICATIONS-SUMMARY.md b/docs/ENHANCED-SPECIFICATIONS-SUMMARY.md deleted file mode 100644 index 7e84497..0000000 --- a/docs/ENHANCED-SPECIFICATIONS-SUMMARY.md +++ /dev/null @@ -1,631 +0,0 @@ -# AI-Trader API Service - Enhanced Specifications Summary - -## Changes from Original Specifications - -Based on user feedback, the specifications have been enhanced with: - -1. **SQLite-backed results storage** (instead of reading position.jsonl on-demand) -2. **Comprehensive Python testing suite** with pytest -3. **Defined testing thresholds** for coverage, performance, and quality gates - ---- - -## Document Index - -### Core Specifications (Original) -1. **[api-specification.md](./api-specification.md)** - REST API endpoints and data models -2. **[job-manager-specification.md](./job-manager-specification.md)** - Job tracking and database layer -3. **[worker-specification.md](./worker-specification.md)** - Background worker architecture -4. **[implementation-specifications.md](./implementation-specifications.md)** - Agent, Docker, Windmill integration - -### Enhanced Specifications (New) -5. **[database-enhanced-specification.md](./database-enhanced-specification.md)** - SQLite results storage -6. **[testing-specification.md](./testing-specification.md)** - Comprehensive testing suite - -### Summary Documents -7. **[README-SPECS.md](./README-SPECS.md)** - Original specifications overview -8. **[ENHANCED-SPECIFICATIONS-SUMMARY.md](./ENHANCED-SPECIFICATIONS-SUMMARY.md)** - This document - ---- - -## Key Enhancement #1: SQLite Results Storage - -### What Changed - -**Before:** -- `/results` endpoint reads `position.jsonl` files on-demand -- File I/O on every API request -- No support for advanced queries (date ranges, aggregations) - -**After:** -- Simulation results written to SQLite during execution -- Fast database queries (10-100x faster than file I/O) -- Advanced analytics: timeseries, leaderboards, aggregations - -### New Database Tables - -```sql --- Results storage -CREATE TABLE positions ( - id INTEGER PRIMARY KEY, - job_id TEXT, - date TEXT, - model TEXT, - action_id INTEGER, - action_type TEXT, - symbol TEXT, - amount INTEGER, - price REAL, - cash REAL, - portfolio_value REAL, - daily_profit REAL, - daily_return_pct REAL, - cumulative_profit REAL, - cumulative_return_pct REAL, - created_at TEXT, - FOREIGN KEY (job_id) REFERENCES jobs(job_id) -); - -CREATE TABLE holdings ( - id INTEGER PRIMARY KEY, - position_id INTEGER, - symbol TEXT, - quantity INTEGER, - FOREIGN KEY (position_id) REFERENCES positions(id) -); - -CREATE TABLE reasoning_logs ( - id INTEGER PRIMARY KEY, - job_id TEXT, - date TEXT, - model TEXT, - step_number INTEGER, - timestamp TEXT, - role TEXT, - content TEXT, - tool_name TEXT, - FOREIGN KEY (job_id) REFERENCES jobs(job_id) -); - -CREATE TABLE tool_usage ( - id INTEGER PRIMARY KEY, - job_id TEXT, - date TEXT, - model TEXT, - tool_name TEXT, - call_count INTEGER, - total_duration_seconds REAL, - FOREIGN KEY (job_id) REFERENCES jobs(job_id) -); -``` - -### New API Endpoints - -```python -# Enhanced results endpoint (now reads from SQLite) -GET /results?date=2025-01-16&model=gpt-5&detail=minimal|full - -# New analytics endpoints -GET /portfolio/timeseries?model=gpt-5&start_date=2025-01-01&end_date=2025-01-31 -GET /leaderboard?date=2025-01-16 # Rankings by portfolio value -``` - -### Migration Strategy - -**Phase 1:** Dual-write mode -- Agent writes to `position.jsonl` (existing code) -- Executor writes to SQLite after agent completes -- Ensures backward compatibility - -**Phase 2:** Verification -- Compare SQLite data vs JSONL data -- Fix any discrepancies - -**Phase 3:** Switch over -- `/results` endpoint reads from SQLite -- JSONL writes become optional (can deprecate later) - -### Performance Improvement - -| Operation | Before (JSONL) | After (SQLite) | Speedup | -|-----------|----------------|----------------|---------| -| Get results for 1 date | 200-500ms | 20-50ms | **10x faster** | -| Get timeseries (30 days) | 6-15 seconds | 100-300ms | **50x faster** | -| Get leaderboard | 5-10 seconds | 50-100ms | **100x faster** | - ---- - -## Key Enhancement #2: Comprehensive Testing Suite - -### Testing Thresholds - -| Metric | Minimum | Target | Enforcement | -|--------|---------|--------|-------------| -| **Code Coverage** | 85% | 90% | CI fails if below | -| **Critical Path Coverage** | 90% | 95% | Manual review | -| **Unit Test Speed** | <10s | <5s | Benchmark tracking | -| **Integration Test Speed** | <60s | <30s | Benchmark tracking | -| **API Response Times** | <500ms | <200ms | Load testing | - -### Test Suite Structure - -``` -tests/ -โ”œโ”€โ”€ unit/ # 80 tests, <10 seconds -โ”‚ โ”œโ”€โ”€ test_job_manager.py # 95% coverage target -โ”‚ โ”œโ”€โ”€ test_database.py -โ”‚ โ”œโ”€โ”€ test_runtime_manager.py -โ”‚ โ”œโ”€โ”€ test_results_service.py # 95% coverage target -โ”‚ โ””โ”€โ”€ test_models.py -โ”‚ -โ”œโ”€โ”€ integration/ # 30 tests, <60 seconds -โ”‚ โ”œโ”€โ”€ test_api_endpoints.py # Full FastAPI testing -โ”‚ โ”œโ”€โ”€ test_worker.py -โ”‚ โ”œโ”€โ”€ test_executor.py -โ”‚ โ””โ”€โ”€ test_end_to_end.py -โ”‚ -โ”œโ”€โ”€ performance/ # 20 tests -โ”‚ โ”œโ”€โ”€ test_database_benchmarks.py -โ”‚ โ”œโ”€โ”€ test_api_load.py # Locust load testing -โ”‚ โ””โ”€โ”€ test_simulation_timing.py -โ”‚ -โ”œโ”€โ”€ security/ # 10 tests -โ”‚ โ”œโ”€โ”€ test_api_security.py # SQL injection, XSS, path traversal -โ”‚ โ””โ”€โ”€ test_auth.py # Future: API key validation -โ”‚ -โ””โ”€โ”€ e2e/ # 10 tests, Docker required - โ””โ”€โ”€ test_docker_workflow.py # Full Docker compose scenario -``` - -### Quality Gates - -**All PRs must pass:** -1. โœ… All tests passing (unit + integration) -2. โœ… Code coverage โ‰ฅ 85% -3. โœ… No critical security vulnerabilities (Bandit scan) -4. โœ… Linting passes (Ruff or Flake8) -5. โœ… Type checking passes (mypy strict mode) -6. โœ… No performance regressions (ยฑ10% tolerance) - -**Release checklist:** -1. โœ… All quality gates pass -2. โœ… End-to-end tests pass in Docker -3. โœ… Load testing passes (100 concurrent requests) -4. โœ… Security scan passes (OWASP ZAP) -5. โœ… Manual smoke tests complete - -### CI/CD Integration - -```yaml -# .github/workflows/test.yml -name: Test Suite - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Run unit tests - run: pytest tests/unit/ --cov=api --cov-fail-under=85 - - name: Run integration tests - run: pytest tests/integration/ - - name: Security scan - run: bandit -r api/ -ll - - name: Upload coverage - uses: codecov/codecov-action@v3 -``` - -### Test Coverage Breakdown - -| Component | Minimum | Target | Tests | -|-----------|---------|--------|-------| -| `api/job_manager.py` | 90% | 95% | 25 tests | -| `api/worker.py` | 85% | 90% | 15 tests | -| `api/executor.py` | 85% | 90% | 12 tests | -| `api/results_service.py` | 90% | 95% | 18 tests | -| `api/database.py` | 95% | 100% | 10 tests | -| `api/runtime_manager.py` | 85% | 90% | 8 tests | -| `api/main.py` | 80% | 85% | 20 tests | -| **Total** | **85%** | **90%** | **~150 tests** | - ---- - -## Updated Implementation Plan - -### Phase 1: API Foundation (Days 1-2) -- [x] Create `api/` directory structure -- [ ] Implement `api/models.py` with Pydantic models -- [ ] Implement `api/database.py` with **enhanced schema** (6 tables) -- [ ] Implement `api/job_manager.py` with job CRUD operations -- [ ] **NEW:** Write unit tests for job_manager (target: 95% coverage) -- [ ] Test database operations manually - -**Testing Deliverables:** -- 25 unit tests for job_manager -- 10 unit tests for database utilities -- 85%+ coverage for Phase 1 code - ---- - -### Phase 2: Worker & Executor (Days 3-4) -- [ ] Implement `api/runtime_manager.py` -- [ ] Implement `api/executor.py` for single model-day execution -- [ ] **NEW:** Add SQLite write logic to executor (`_store_results_to_db()`) -- [ ] Implement `api/worker.py` for job orchestration -- [ ] **NEW:** Write unit tests for worker and executor (target: 85% coverage) -- [ ] Test runtime config isolation - -**Testing Deliverables:** -- 15 unit tests for worker -- 12 unit tests for executor -- 8 unit tests for runtime_manager -- 85%+ coverage for Phase 2 code - ---- - -### Phase 3: Results Service & FastAPI Endpoints (Days 5-6) -- [ ] **NEW:** Implement `api/results_service.py` (SQLite-backed) - - [ ] `get_results(date, model, detail)` - - [ ] `get_portfolio_timeseries(model, start_date, end_date)` - - [ ] `get_leaderboard(date)` -- [ ] Implement `api/main.py` with all endpoints - - [ ] `/simulate/trigger` with background tasks - - [ ] `/simulate/status/{job_id}` - - [ ] `/simulate/current` - - [ ] `/results` (now reads from SQLite) - - [ ] **NEW:** `/portfolio/timeseries` - - [ ] **NEW:** `/leaderboard` - - [ ] `/health` with MCP checks -- [ ] **NEW:** Write unit tests for results_service (target: 95% coverage) -- [ ] **NEW:** Write integration tests for API endpoints (target: 80% coverage) -- [ ] Test all endpoints with Postman/curl - -**Testing Deliverables:** -- 18 unit tests for results_service -- 20 integration tests for API endpoints -- Performance benchmarks for database queries -- 85%+ coverage for Phase 3 code - ---- - -### Phase 4: Docker Integration (Day 7) -- [ ] Update `Dockerfile` -- [ ] Create `docker-entrypoint-api.sh` -- [ ] Create `requirements-api.txt` -- [ ] Update `docker-compose.yml` -- [ ] Test Docker build -- [ ] Test container startup and health checks -- [ ] **NEW:** Run E2E tests in Docker environment -- [ ] Test end-to-end simulation via API in Docker - -**Testing Deliverables:** -- 10 E2E tests with Docker -- Docker health check validation -- Performance testing in containerized environment - ---- - -### Phase 5: Windmill Integration (Days 8-9) -- [ ] Create Windmill scripts (trigger, poll, store) -- [ ] **UPDATED:** Modify `store_simulation_results.py` to use new `/results` endpoint -- [ ] Test scripts locally against Docker API -- [ ] Deploy scripts to Windmill instance -- [ ] Create Windmill workflow -- [ ] Test workflow end-to-end -- [ ] Create Windmill dashboard (using new `/portfolio/timeseries` and `/leaderboard` endpoints) -- [ ] Document Windmill setup process - -**Testing Deliverables:** -- Integration tests for Windmill scripts -- End-to-end workflow validation -- Dashboard functionality verification - ---- - -### Phase 6: Testing, Security & Documentation (Day 10) -- [ ] **NEW:** Run full test suite and verify all thresholds met - - [ ] Code coverage โ‰ฅ 85% - - [ ] All ~150 tests passing - - [ ] Performance benchmarks within limits -- [ ] **NEW:** Security testing - - [ ] Bandit scan (Python security issues) - - [ ] SQL injection tests - - [ ] Input validation tests - - [ ] OWASP ZAP scan (optional) -- [ ] **NEW:** Load testing with Locust - - [ ] 100 concurrent users - - [ ] API endpoints within performance thresholds -- [ ] Integration tests for complete workflow -- [ ] Update README.md with API usage -- [ ] Create API documentation (Swagger/OpenAPI - auto-generated by FastAPI) -- [ ] Create deployment guide -- [ ] Create troubleshooting guide -- [ ] **NEW:** Generate test coverage report - -**Testing Deliverables:** -- Full test suite execution report -- Security scan results -- Load testing results -- Coverage report (HTML + XML) -- CI/CD pipeline configuration - ---- - -## New Files Created - -### Database & Results -- `api/results_service.py` - SQLite-backed results retrieval -- `api/import_historical_data.py` - Migration script for existing position.jsonl files - -### Testing Suite -- `tests/conftest.py` - Shared pytest fixtures -- `tests/unit/test_job_manager.py` - 25 tests -- `tests/unit/test_database.py` - 10 tests -- `tests/unit/test_runtime_manager.py` - 8 tests -- `tests/unit/test_results_service.py` - 18 tests -- `tests/unit/test_models.py` - 5 tests -- `tests/integration/test_api_endpoints.py` - 20 tests -- `tests/integration/test_worker.py` - 15 tests -- `tests/integration/test_executor.py` - 12 tests -- `tests/integration/test_end_to_end.py` - 5 tests -- `tests/performance/test_database_benchmarks.py` - 10 tests -- `tests/performance/test_api_load.py` - Locust load testing -- `tests/security/test_api_security.py` - 10 tests -- `tests/e2e/test_docker_workflow.py` - 10 tests -- `pytest.ini` - Test configuration -- `requirements-dev.txt` - Testing dependencies - -### CI/CD -- `.github/workflows/test.yml` - GitHub Actions workflow - ---- - -## Updated File Structure - -``` -AI-Trader/ -โ”œโ”€โ”€ api/ -โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”œโ”€โ”€ main.py # FastAPI application -โ”‚ โ”œโ”€โ”€ models.py # Pydantic request/response models -โ”‚ โ”œโ”€โ”€ job_manager.py # Job lifecycle management -โ”‚ โ”œโ”€โ”€ database.py # SQLite utilities (enhanced schema) -โ”‚ โ”œโ”€โ”€ worker.py # Background simulation worker -โ”‚ โ”œโ”€โ”€ executor.py # Single model-day execution (+ SQLite writes) -โ”‚ โ”œโ”€โ”€ runtime_manager.py # Runtime config isolation -โ”‚ โ”œโ”€โ”€ results_service.py # NEW: SQLite-backed results retrieval -โ”‚ โ””โ”€โ”€ import_historical_data.py # NEW: JSONL โ†’ SQLite migration -โ”‚ -โ”œโ”€โ”€ tests/ # NEW: Comprehensive test suite -โ”‚ โ”œโ”€โ”€ conftest.py -โ”‚ โ”œโ”€โ”€ unit/ # 80 tests, <10s -โ”‚ โ”œโ”€โ”€ integration/ # 30 tests, <60s -โ”‚ โ”œโ”€โ”€ performance/ # 20 tests -โ”‚ โ”œโ”€โ”€ security/ # 10 tests -โ”‚ โ””โ”€โ”€ e2e/ # 10 tests -โ”‚ -โ”œโ”€โ”€ docs/ -โ”‚ โ”œโ”€โ”€ api-specification.md -โ”‚ โ”œโ”€โ”€ job-manager-specification.md -โ”‚ โ”œโ”€โ”€ worker-specification.md -โ”‚ โ”œโ”€โ”€ implementation-specifications.md -โ”‚ โ”œโ”€โ”€ database-enhanced-specification.md # NEW -โ”‚ โ”œโ”€โ”€ testing-specification.md # NEW -โ”‚ โ”œโ”€โ”€ README-SPECS.md -โ”‚ โ””โ”€โ”€ ENHANCED-SPECIFICATIONS-SUMMARY.md # NEW (this file) -โ”‚ -โ”œโ”€โ”€ data/ -โ”‚ โ”œโ”€โ”€ jobs.db # SQLite database (6 tables) -โ”‚ โ”œโ”€โ”€ runtime_env*.json # Runtime configs (temporary) -โ”‚ โ”œโ”€โ”€ agent_data/ # Existing position/log data -โ”‚ โ””โ”€โ”€ merged.jsonl # Existing price data -โ”‚ -โ”œโ”€โ”€ pytest.ini # NEW: Test configuration -โ”œโ”€โ”€ requirements-dev.txt # NEW: Testing dependencies -โ”œโ”€โ”€ .github/workflows/test.yml # NEW: CI/CD pipeline -โ””โ”€โ”€ ... (existing files) -``` - ---- - -## Benefits Summary - -### Performance -- **10-100x faster** results queries (SQLite vs file I/O) -- **Advanced analytics** - timeseries, leaderboards, aggregations in milliseconds -- **Optimized indexes** for common queries - -### Quality -- **85% minimum coverage** enforced by CI/CD -- **150 comprehensive tests** across unit, integration, performance, security -- **Quality gates** prevent regressions -- **Type safety** with mypy strict mode - -### Maintainability -- **SQLite single source of truth** - easier backup, restore, migration -- **Automated testing** catches bugs early -- **CI/CD integration** provides fast feedback on every commit -- **Security scanning** prevents vulnerabilities - -### Analytics Capabilities - -**New queries enabled by SQLite:** - -```python -# Portfolio timeseries for charting -GET /portfolio/timeseries?model=gpt-5&start_date=2025-01-01&end_date=2025-01-31 - -# Model leaderboard -GET /leaderboard?date=2025-01-31 - -# Advanced filtering (future) -SELECT * FROM positions -WHERE daily_return_pct > 2.0 -ORDER BY portfolio_value DESC; - -# Aggregations (future) -SELECT model, AVG(daily_return_pct) as avg_return -FROM positions -GROUP BY model -ORDER BY avg_return DESC; -``` - ---- - -## Migration from Original Spec - -If you've already started implementation based on original specs: - -### Step 1: Database Schema Migration -```sql --- Run enhanced schema creation --- See database-enhanced-specification.md Section 2.1 -``` - -### Step 2: Add Results Service -```bash -# Create new file -touch api/results_service.py -# Implement as per database-enhanced-specification.md Section 4.1 -``` - -### Step 3: Update Executor -```python -# In api/executor.py, add after agent.run_trading_session(): -self._store_results_to_db(job_id, date, model_sig) -``` - -### Step 4: Update API Endpoints -```python -# In api/main.py, update /results endpoint to use ResultsService -from api.results_service import ResultsService -results_service = ResultsService() - -@app.get("/results") -async def get_results(...): - return results_service.get_results(date, model, detail) -``` - -### Step 5: Add Test Suite -```bash -mkdir -p tests/{unit,integration,performance,security,e2e} -# Create test files as per testing-specification.md Section 4-8 -``` - -### Step 6: Configure CI/CD -```bash -mkdir -p .github/workflows -# Create test.yml as per testing-specification.md Section 10.1 -``` - ---- - -## Testing Execution Guide - -### Run Unit Tests -```bash -pytest tests/unit/ -v --cov=api --cov-report=term-missing -``` - -### Run Integration Tests -```bash -pytest tests/integration/ -v -``` - -### Run All Tests (Except E2E) -```bash -pytest tests/ -v --ignore=tests/e2e/ --cov=api --cov-report=html -``` - -### Run E2E Tests (Requires Docker) -```bash -pytest tests/e2e/ -v -s -``` - -### Run Performance Benchmarks -```bash -pytest tests/performance/ --benchmark-only -``` - -### Run Security Tests -```bash -pytest tests/security/ -v -bandit -r api/ -ll -``` - -### Generate Coverage Report -```bash -pytest tests/unit/ tests/integration/ --cov=api --cov-report=html -open htmlcov/index.html # View in browser -``` - -### Run Load Tests -```bash -locust -f tests/performance/test_api_load.py --host=http://localhost:8080 -# Open http://localhost:8089 for Locust UI -``` - ---- - -## Questions & Next Steps - -### Review Checklist - -Please review: -1. โœ… **Enhanced database schema** with 6 tables for comprehensive results storage -2. โœ… **Migration strategy** for backward compatibility (dual-write mode) -3. โœ… **Testing thresholds** (85% coverage minimum, performance benchmarks) -4. โœ… **Test suite structure** (150 tests across 5 categories) -5. โœ… **CI/CD integration** with quality gates -6. โœ… **Updated implementation plan** (10 days, 6 phases) - -### Questions to Consider - -1. **Database migration timing:** Start with dual-write mode immediately, or add in Phase 2? -2. **Testing priorities:** Should we implement tests alongside features (TDD) or after each phase? -3. **CI/CD platform:** GitHub Actions (as specified) or different platform? -4. **Performance baselines:** Should we run benchmarks before implementation to track improvement? -5. **Security priorities:** Which security tests are MVP vs nice-to-have? - -### Ready to Implement? - -**Option A:** Approve specifications and begin Phase 1 implementation -- Create API directory structure -- Implement enhanced database schema -- Write unit tests for database layer -- Target: 2 days, 90%+ coverage for database code - -**Option B:** Request modifications to specifications -- Clarify any unclear requirements -- Adjust testing thresholds -- Modify implementation timeline - -**Option C:** Implement in parallel workstreams -- Workstream 1: Core API (Phases 1-3) -- Workstream 2: Testing suite (parallel with Phase 1-3) -- Workstream 3: Docker + Windmill (Phases 4-5) -- Benefits: Faster delivery, more parallelization -- Requires: Clear interfaces between components - ---- - -## Summary - -**Enhanced specifications** add: -1. ๐Ÿ—„๏ธ **SQLite results storage** - 10-100x faster queries, advanced analytics -2. ๐Ÿงช **Comprehensive testing** - 150 tests, 85% coverage, quality gates -3. ๐Ÿ”’ **Security testing** - SQL injection, XSS, input validation -4. โšก **Performance benchmarks** - Catch regressions early -5. ๐Ÿš€ **CI/CD pipeline** - Automated quality checks on every commit - -**Total effort:** Still ~10 days, but with significantly higher code quality and confidence in deployments. - -**Risk mitigation:** Extensive testing catches bugs before production, preventing costly hotfixes. - -**Long-term value:** Maintainable, well-tested codebase enables rapid feature development. - ---- - -Ready to proceed? Please provide feedback or approval to begin implementation! diff --git a/docs/README-SPECS.md b/docs/README-SPECS.md deleted file mode 100644 index c9fa584..0000000 --- a/docs/README-SPECS.md +++ /dev/null @@ -1,436 +0,0 @@ -# AI-Trader API Service - Technical Specifications Summary - -## Overview - -This directory contains comprehensive technical specifications for transforming the AI-Trader batch simulation system into an API service compatible with Windmill automation. - -## Specification Documents - -### 1. [API Specification](./api-specification.md) -**Purpose:** Defines all API endpoints, request/response formats, and data models - -**Key Contents:** -- **5 REST Endpoints:** - - `POST /simulate/trigger` - Queue catch-up simulation job - - `GET /simulate/status/{job_id}` - Poll job progress - - `GET /simulate/current` - Get latest job - - `GET /results` - Retrieve simulation results (minimal/full detail) - - `GET /health` - Service health check -- **Pydantic Models** for type-safe request/response handling -- **Error Handling** strategies and HTTP status codes -- **SQLite Schema** for jobs and job_details tables -- **Configuration Management** via environment variables - -**Status Codes:** 200 OK, 202 Accepted, 400 Bad Request, 404 Not Found, 409 Conflict, 503 Service Unavailable - ---- - -### 2. [Job Manager Specification](./job-manager-specification.md) -**Purpose:** Details the job tracking and database layer - -**Key Contents:** -- **SQLite Database Schema:** - - `jobs` table - High-level job metadata - - `job_details` table - Per model-day execution tracking -- **JobManager Class Interface:** - - `create_job()` - Create new simulation job - - `get_job()` - Retrieve job by ID - - `update_job_status()` - State transitions (pending โ†’ running โ†’ completed/partial/failed) - - `get_job_progress()` - Detailed progress metrics - - `can_start_new_job()` - Concurrency control -- **State Machine:** Job status transitions and business logic -- **Concurrency Control:** Single-job execution enforcement -- **Testing Strategy:** Unit tests with temporary databases - -**Key Feature:** Independent model execution - one model's failure doesn't block others (results in "partial" status) - ---- - -### 3. [Background Worker Specification](./worker-specification.md) -**Purpose:** Defines async job execution architecture - -**Key Contents:** -- **Execution Pattern:** Date-sequential, Model-parallel - - All models for Date 1 run in parallel - - Date 2 starts only after all models finish Date 1 - - Ensures position.jsonl integrity (no concurrent writes) -- **SimulationWorker Class:** - - Orchestrates job execution - - Manages date sequencing - - Handles job-level errors -- **ModelDayExecutor Class:** - - Executes single model-day simulation - - Updates job_detail status - - Isolates runtime configuration -- **RuntimeConfigManager:** - - Creates temporary runtime_env_{job_id}_{model}_{date}.json files - - Prevents state collisions between concurrent models - - Cleans up after execution -- **Error Handling:** Graceful failure (models continue despite peer failures) -- **Logging:** Structured JSON logging with job/model/date context - -**Performance:** 3 models ร— 5 days = ~7-15 minutes (vs. ~22-45 minutes sequential) - ---- - -### 4. [Implementation Specification](./implementation-specifications.md) -**Purpose:** Complete implementation guide covering Agent, Docker, and Windmill - -**Key Contents:** - -#### Part 1: BaseAgent Refactoring -- **Analysis:** Existing `run_trading_session()` already compatible with API mode -- **Required Changes:** โœ… NONE! Existing code works as-is -- **Worker Integration:** Calls `agent.run_trading_session(date)` directly - -#### Part 2: Docker Configuration -- **Modified Dockerfile:** Adds FastAPI dependencies, new entrypoint -- **docker-entrypoint-api.sh:** Starts MCP services โ†’ launches uvicorn -- **Health Checks:** Verifies MCP services and database connectivity -- **Volume Mounts:** `./data`, `./configs` for persistence - -#### Part 3: Windmill Integration -- **Flow 1: trigger_simulation.ts** - Daily cron triggers API -- **Flow 2: poll_simulation_status.ts** - Polls every 5 min until complete -- **Flow 3: store_simulation_results.py** - Stores results in Windmill DB -- **Dashboard:** Charts and tables showing portfolio performance -- **Workflow Orchestration:** Complete YAML workflow definition - -#### Part 4: File Structure -- New `api/` directory with 7 modules -- New `windmill/` directory with scripts and dashboard -- New `docs/` directory (this folder) -- `data/jobs.db` for job tracking - -#### Part 5: Implementation Checklist -10-day implementation plan broken into 6 phases - ---- - -## Architecture Highlights - -### Request Flow - -``` -1. Windmill โ†’ POST /simulate/trigger -2. API creates job in SQLite (status: pending) -3. API queues BackgroundTask -4. API returns 202 Accepted with job_id - โ†“ -5. Worker starts (status: running) -6. For each date sequentially: - For each model in parallel: - - Create isolated runtime config - - Execute agent.run_trading_session(date) - - Update job_detail status -7. Worker finishes (status: completed/partial/failed) - โ†“ -8. Windmill polls GET /simulate/status/{job_id} -9. When complete: Windmill calls GET /results?date=X -10. Windmill stores results in internal DB -11. Windmill dashboard displays performance -``` - -### Data Flow - -``` -Input: configs/default_config.json - โ†“ -API: Calculates date_range (last position โ†’ today) - โ†“ -Worker: Executes simulations - โ†“ -Output: data/agent_data/{model}/position/position.jsonl - data/agent_data/{model}/log/{date}/log.jsonl - data/jobs.db (job tracking) - โ†“ -API: Reads position.jsonl + calculates P&L - โ†“ -Windmill: Stores in internal DB โ†’ Dashboard visualization -``` - ---- - -## Key Design Decisions - -### 1. Pattern B: Lazy On-Demand Processing -- **Chosen:** Windmill controls simulation timing via API calls -- **Benefit:** Centralized scheduling in Windmill -- **Tradeoff:** First Windmill call of the day triggers long-running job - -### 2. SQLite vs. PostgreSQL -- **Chosen:** SQLite for MVP -- **Rationale:** Low concurrency (1 job at a time), simple deployment -- **Future:** PostgreSQL for production with multiple concurrent jobs - -### 3. Date-Sequential, Model-Parallel Execution -- **Chosen:** Dates run sequentially, models run in parallel per date -- **Rationale:** Prevents position.jsonl race conditions, faster than fully sequential -- **Performance:** ~50% faster than sequential (3 models in parallel) - -### 4. Independent Model Failures -- **Chosen:** One model's failure doesn't block others -- **Benefit:** Partial results better than no results -- **Implementation:** Job status becomes "partial" if any model fails - -### 5. Minimal BaseAgent Changes -- **Chosen:** No modifications to agent code -- **Rationale:** Existing `run_trading_session()` is perfect API interface -- **Benefit:** Maintains backward compatibility with batch mode - ---- - -## Implementation Prerequisites - -### Required Environment Variables -```bash -OPENAI_API_BASE=... -OPENAI_API_KEY=... -ALPHAADVANTAGE_API_KEY=... -JINA_API_KEY=... -RUNTIME_ENV_PATH=/app/data/runtime_env.json -MATH_HTTP_PORT=8000 -SEARCH_HTTP_PORT=8001 -TRADE_HTTP_PORT=8002 -GETPRICE_HTTP_PORT=8003 -API_HOST=0.0.0.0 -API_PORT=8080 -``` - -### Required Python Packages (new) -``` -fastapi==0.109.0 -uvicorn[standard]==0.27.0 -pydantic==2.5.3 -``` - -### Docker Requirements -- Docker Engine 20.10+ -- Docker Compose 2.0+ -- 2GB RAM minimum for container -- 10GB disk space for data - -### Windmill Requirements -- Windmill instance (self-hosted or cloud) -- Network access from Windmill to AI-Trader API -- Windmill CLI for deployment (optional) - ---- - -## Testing Strategy - -### Unit Tests -- `tests/test_job_manager.py` - Database operations -- `tests/test_worker.py` - Job execution logic -- `tests/test_executor.py` - Model-day execution - -### Integration Tests -- `tests/test_api_endpoints.py` - FastAPI endpoint behavior -- `tests/test_end_to_end.py` - Full workflow (trigger โ†’ execute โ†’ retrieve) - -### Manual Testing -- Docker container startup -- Health check endpoint -- Windmill workflow execution -- Dashboard visualization - ---- - -## Performance Expectations - -### Single Model-Day Execution -- **Duration:** 30-60 seconds (varies by AI model latency) -- **Bottlenecks:** AI API calls, MCP tool latency - -### Multi-Model Job -- **Example:** 3 models ร— 5 days = 15 model-days -- **Parallel Execution:** ~7-15 minutes -- **Sequential Execution:** ~22-45 minutes -- **Speedup:** ~3x (number of models) - -### API Response Times -- `/simulate/trigger`: < 1 second (just queues job) -- `/simulate/status`: < 100ms (SQLite query) -- `/results?detail=minimal`: < 500ms (file read + JSON parsing) -- `/results?detail=full`: < 2 seconds (parse log files) - ---- - -## Security Considerations - -### MVP Security -- **Network Isolation:** Docker network (no public exposure) -- **No Authentication:** Assumes Windmill โ†’ API is trusted network - -### Future Enhancements -- API key authentication (`X-API-Key` header) -- Rate limiting per client -- HTTPS/TLS encryption -- Input sanitization for path traversal prevention - ---- - -## Deployment Steps - -### 1. Build Docker Image -```bash -docker-compose build -``` - -### 2. Start API Service -```bash -docker-compose up -d -``` - -### 3. Verify Health -```bash -curl http://localhost:8080/health -``` - -### 4. Test Trigger -```bash -curl -X POST http://localhost:8080/simulate/trigger \ - -H "Content-Type: application/json" \ - -d '{"config_path": "configs/default_config.json"}' -``` - -### 5. Deploy Windmill Scripts -```bash -wmill script push windmill/trigger_simulation.ts -wmill script push windmill/poll_simulation_status.ts -wmill script push windmill/store_simulation_results.py -``` - -### 6. Create Windmill Workflow -- Import `windmill/daily_simulation_workflow.yaml` -- Configure resource `ai_trader_api` with API URL -- Set cron schedule (daily 6 AM) - -### 7. Create Windmill Dashboard -- Import `windmill/dashboard.json` -- Verify data visualization - ---- - -## Troubleshooting Guide - -### Issue: Health check fails -**Symptoms:** `curl http://localhost:8080/health` returns 503 - -**Possible Causes:** -1. MCP services not running -2. Database file permission error -3. API server not started - -**Solutions:** -```bash -# Check MCP services -docker-compose exec ai-trader curl http://localhost:8000/health - -# Check API logs -docker-compose logs -f ai-trader - -# Restart container -docker-compose restart -``` - -### Issue: Job stuck in "running" status -**Symptoms:** Job never completes, status remains "running" - -**Possible Causes:** -1. Agent execution crashed -2. Model API timeout -3. Worker process died - -**Solutions:** -```bash -# Check job details for error messages -curl http://localhost:8080/simulate/status/{job_id} - -# Check container logs -docker-compose logs -f ai-trader - -# If API restarted, stale jobs are marked as failed on startup -docker-compose restart -``` - -### Issue: Windmill can't reach API -**Symptoms:** Connection refused from Windmill scripts - -**Solutions:** -- Verify Windmill and AI-Trader on same Docker network -- Check firewall rules -- Use container name (ai-trader) instead of localhost in Windmill resource -- Verify API_PORT environment variable - ---- - -## Migration from Batch Mode - -### For Users Currently Running Batch Mode - -**Option 1: Dual Mode (Recommended)** -- Keep existing `main.py` for manual testing -- Add new API mode for production automation -- Use different config files for each mode - -**Option 2: API-Only** -- Replace batch execution entirely -- All simulations via API calls -- More consistent with production workflow - -### Migration Checklist -- [ ] Backup existing `data/` directory -- [ ] Update `.env` with API configuration -- [ ] Test API mode in separate environment first -- [ ] Gradually migrate Windmill workflows -- [ ] Monitor logs for errors -- [ ] Validate results match batch mode output - ---- - -## Next Steps - -1. **Review Specifications** - - Read all 4 specification documents - - Ask clarifying questions - - Approve design before implementation - -2. **Implementation Phase 1** (Days 1-2) - - Set up `api/` directory structure - - Implement database and job_manager - - Write unit tests - -3. **Implementation Phase 2** (Days 3-4) - - Implement worker and executor - - Test with mock agents - -4. **Implementation Phase 3** (Days 5-6) - - Implement FastAPI endpoints - - Test with Postman/curl - -5. **Implementation Phase 4** (Day 7) - - Docker integration - - End-to-end testing - -6. **Implementation Phase 5** (Days 8-9) - - Windmill integration - - Dashboard creation - -7. **Implementation Phase 6** (Day 10) - - Final testing - - Documentation - ---- - -## Questions or Feedback? - -Please review all specifications and provide feedback on: -1. API endpoint design -2. Database schema -3. Execution pattern (date-sequential, model-parallel) -4. Error handling approach -5. Windmill integration workflow -6. Any concerns or suggested improvements - -**Ready to proceed with implementation?** Confirm approval of specifications to begin Phase 1. diff --git a/docs/api-specification.md b/docs/api-specification.md deleted file mode 100644 index 73a8acc..0000000 --- a/docs/api-specification.md +++ /dev/null @@ -1,837 +0,0 @@ -# AI-Trader API Service - Technical Specification - -## 1. API Endpoints Specification - -### 1.1 POST /simulate/trigger - -**Purpose:** Trigger a catch-up simulation from the last completed date to the most recent trading day. - -**Request:** -```http -POST /simulate/trigger HTTP/1.1 -Content-Type: application/json - -{ - "config_path": "configs/default_config.json" // Optional: defaults to configs/default_config.json -} -``` - -**Response (202 Accepted):** -```json -{ - "job_id": "550e8400-e29b-41d4-a716-446655440000", - "status": "accepted", - "date_range": ["2025-01-16", "2025-01-17", "2025-01-20"], - "models": ["claude-3.7-sonnet", "gpt-5"], - "created_at": "2025-01-20T14:30:00Z", - "message": "Simulation job queued successfully" -} -``` - -**Response (200 OK - Job Already Running):** -```json -{ - "job_id": "550e8400-e29b-41d4-a716-446655440000", - "status": "running", - "date_range": ["2025-01-16", "2025-01-17", "2025-01-20"], - "models": ["claude-3.7-sonnet", "gpt-5"], - "progress": { - "total_model_days": 6, - "completed": 3, - "failed": 0, - "current": { - "date": "2025-01-17", - "model": "gpt-5" - } - }, - "created_at": "2025-01-20T14:25:00Z", - "message": "Simulation already in progress" -} -``` - -**Response (200 OK - Already Up To Date):** -```json -{ - "status": "current", - "message": "Simulation already up-to-date", - "last_simulation_date": "2025-01-20", - "next_trading_day": "2025-01-21" -} -``` - -**Response (409 Conflict):** -```json -{ - "error": "conflict", - "message": "Different simulation already running", - "current_job_id": "previous-job-uuid", - "current_date_range": ["2025-01-10", "2025-01-15"] -} -``` - -**Business Logic:** -1. Load configuration from `config_path` (or default) -2. Determine last completed date from each model's `position.jsonl` -3. Calculate date range: `max(last_dates) + 1 day` โ†’ `most_recent_trading_day` -4. Filter for weekdays only (Monday-Friday) -5. If date_range is empty, return "already up-to-date" -6. Check for existing jobs with same date range โ†’ return existing job -7. Check for running jobs with different date range โ†’ return 409 -8. Create new job in SQLite with status=`pending` -9. Queue background task to execute simulation -10. Return 202 with job details - ---- - -### 1.2 GET /simulate/status/{job_id} - -**Purpose:** Poll the status and progress of a simulation job. - -**Request:** -```http -GET /simulate/status/550e8400-e29b-41d4-a716-446655440000 HTTP/1.1 -``` - -**Response (200 OK - Running):** -```json -{ - "job_id": "550e8400-e29b-41d4-a716-446655440000", - "status": "running", - "date_range": ["2025-01-16", "2025-01-17", "2025-01-20"], - "models": ["claude-3.7-sonnet", "gpt-5"], - "progress": { - "total_model_days": 6, - "completed": 3, - "failed": 0, - "current": { - "date": "2025-01-17", - "model": "gpt-5" - }, - "details": [ - {"date": "2025-01-16", "model": "claude-3.7-sonnet", "status": "completed", "duration_seconds": 45.2}, - {"date": "2025-01-16", "model": "gpt-5", "status": "completed", "duration_seconds": 38.7}, - {"date": "2025-01-17", "model": "claude-3.7-sonnet", "status": "completed", "duration_seconds": 42.1}, - {"date": "2025-01-17", "model": "gpt-5", "status": "running", "duration_seconds": null} - ] - }, - "created_at": "2025-01-20T14:25:00Z", - "updated_at": "2025-01-20T14:27:15Z" -} -``` - -**Response (200 OK - Completed):** -```json -{ - "job_id": "550e8400-e29b-41d4-a716-446655440000", - "status": "completed", - "date_range": ["2025-01-16", "2025-01-17", "2025-01-20"], - "models": ["claude-3.7-sonnet", "gpt-5"], - "progress": { - "total_model_days": 6, - "completed": 6, - "failed": 0, - "details": [ - {"date": "2025-01-16", "model": "claude-3.7-sonnet", "status": "completed", "duration_seconds": 45.2}, - {"date": "2025-01-16", "model": "gpt-5", "status": "completed", "duration_seconds": 38.7}, - {"date": "2025-01-17", "model": "claude-3.7-sonnet", "status": "completed", "duration_seconds": 42.1}, - {"date": "2025-01-17", "model": "gpt-5", "status": "completed", "duration_seconds": 40.3}, - {"date": "2025-01-20", "model": "claude-3.7-sonnet", "status": "completed", "duration_seconds": 43.8}, - {"date": "2025-01-20", "model": "gpt-5", "status": "completed", "duration_seconds": 39.1} - ] - }, - "created_at": "2025-01-20T14:25:00Z", - "completed_at": "2025-01-20T14:29:45Z", - "total_duration_seconds": 285.0 -} -``` - -**Response (200 OK - Partial Failure):** -```json -{ - "job_id": "550e8400-e29b-41d4-a716-446655440000", - "status": "partial", - "date_range": ["2025-01-16", "2025-01-17", "2025-01-20"], - "models": ["claude-3.7-sonnet", "gpt-5"], - "progress": { - "total_model_days": 6, - "completed": 4, - "failed": 2, - "details": [ - {"date": "2025-01-16", "model": "claude-3.7-sonnet", "status": "completed", "duration_seconds": 45.2}, - {"date": "2025-01-16", "model": "gpt-5", "status": "completed", "duration_seconds": 38.7}, - {"date": "2025-01-17", "model": "claude-3.7-sonnet", "status": "failed", "error": "MCP service timeout after 3 retries", "duration_seconds": null}, - {"date": "2025-01-17", "model": "gpt-5", "status": "completed", "duration_seconds": 40.3}, - {"date": "2025-01-20", "model": "claude-3.7-sonnet", "status": "completed", "duration_seconds": 43.8}, - {"date": "2025-01-20", "model": "gpt-5", "status": "failed", "error": "AI model API timeout", "duration_seconds": null} - ] - }, - "created_at": "2025-01-20T14:25:00Z", - "completed_at": "2025-01-20T14:29:45Z" -} -``` - -**Response (404 Not Found):** -```json -{ - "error": "not_found", - "message": "Job not found", - "job_id": "invalid-job-id" -} -``` - -**Business Logic:** -1. Query SQLite jobs table for job_id -2. If not found, return 404 -3. Return job metadata + progress from job_details table -4. Status transitions: `pending` โ†’ `running` โ†’ `completed`/`partial`/`failed` - ---- - -### 1.3 GET /simulate/current - -**Purpose:** Get the most recent simulation job (for Windmill to discover job_id). - -**Request:** -```http -GET /simulate/current HTTP/1.1 -``` - -**Response (200 OK):** -```json -{ - "job_id": "550e8400-e29b-41d4-a716-446655440000", - "status": "running", - "date_range": ["2025-01-16", "2025-01-17"], - "models": ["claude-3.7-sonnet", "gpt-5"], - "progress": { - "total_model_days": 4, - "completed": 2, - "failed": 0 - }, - "created_at": "2025-01-20T14:25:00Z" -} -``` - -**Response (404 Not Found):** -```json -{ - "error": "not_found", - "message": "No simulation jobs found" -} -``` - -**Business Logic:** -1. Query SQLite: `SELECT * FROM jobs ORDER BY created_at DESC LIMIT 1` -2. Return job details with progress summary - ---- - -### 1.4 GET /results - -**Purpose:** Retrieve simulation results for a specific date and model. - -**Request:** -```http -GET /results?date=2025-01-15&model=gpt-5&detail=minimal HTTP/1.1 -``` - -**Query Parameters:** -- `date` (required): Trading date in YYYY-MM-DD format -- `model` (optional): Model signature (if omitted, returns all models) -- `detail` (optional): Response detail level - - `minimal` (default): Positions + daily P&L - - `full`: + trade history + AI reasoning logs + tool usage stats - -**Response (200 OK - minimal):** -```json -{ - "date": "2025-01-15", - "results": [ - { - "model": "gpt-5", - "positions": { - "AAPL": 10, - "MSFT": 5, - "NVDA": 0, - "CASH": 8500.00 - }, - "daily_pnl": { - "profit": 150.50, - "return_pct": 1.5, - "portfolio_value": 10150.50 - } - } - ] -} -``` - -**Response (200 OK - full):** -```json -{ - "date": "2025-01-15", - "results": [ - { - "model": "gpt-5", - "positions": { - "AAPL": 10, - "MSFT": 5, - "CASH": 8500.00 - }, - "daily_pnl": { - "profit": 150.50, - "return_pct": 1.5, - "portfolio_value": 10150.50 - }, - "trades": [ - { - "id": 1, - "action": "buy", - "symbol": "AAPL", - "amount": 10, - "price": 255.88, - "total": 2558.80 - } - ], - "ai_reasoning": { - "total_steps": 15, - "stop_signal_received": true, - "reasoning_summary": "Market analysis indicated strong buy signal for AAPL...", - "tool_usage": { - "search": 3, - "get_price": 5, - "math": 2, - "trade": 1 - } - }, - "log_file_path": "data/agent_data/gpt-5/log/2025-01-15/log.jsonl" - } - ] -} -``` - -**Response (400 Bad Request):** -```json -{ - "error": "invalid_date", - "message": "Date must be in YYYY-MM-DD format" -} -``` - -**Response (404 Not Found):** -```json -{ - "error": "no_data", - "message": "No simulation data found for date 2025-01-15 and model gpt-5" -} -``` - -**Business Logic:** -1. Validate date format -2. Read `position.jsonl` for specified model(s) and date -3. For `detail=minimal`: Return positions + calculate daily P&L -4. For `detail=full`: - - Parse `log.jsonl` to extract reasoning summary - - Count tool usage from log messages - - Extract trades from position file -5. Return aggregated results - ---- - -### 1.5 GET /health - -**Purpose:** Health check endpoint for Docker and monitoring. - -**Request:** -```http -GET /health HTTP/1.1 -``` - -**Response (200 OK):** -```json -{ - "status": "healthy", - "timestamp": "2025-01-20T14:30:00Z", - "services": { - "mcp_math": {"status": "up", "url": "http://localhost:8000/mcp"}, - "mcp_search": {"status": "up", "url": "http://localhost:8001/mcp"}, - "mcp_trade": {"status": "up", "url": "http://localhost:8002/mcp"}, - "mcp_getprice": {"status": "up", "url": "http://localhost:8003/mcp"} - }, - "storage": { - "data_directory": "/app/data", - "writable": true, - "free_space_mb": 15234 - }, - "database": { - "status": "connected", - "path": "/app/data/jobs.db" - } -} -``` - -**Response (503 Service Unavailable):** -```json -{ - "status": "unhealthy", - "timestamp": "2025-01-20T14:30:00Z", - "services": { - "mcp_math": {"status": "down", "url": "http://localhost:8000/mcp", "error": "Connection refused"}, - "mcp_search": {"status": "up", "url": "http://localhost:8001/mcp"}, - "mcp_trade": {"status": "up", "url": "http://localhost:8002/mcp"}, - "mcp_getprice": {"status": "up", "url": "http://localhost:8003/mcp"} - }, - "storage": { - "data_directory": "/app/data", - "writable": true - }, - "database": { - "status": "connected" - } -} -``` - ---- - -## 2. Data Models - -### 2.1 SQLite Schema - -**Table: jobs** -```sql -CREATE TABLE jobs ( - job_id TEXT PRIMARY KEY, - config_path TEXT NOT NULL, - status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'partial', 'failed')), - date_range TEXT NOT NULL, -- JSON array of dates - models TEXT NOT NULL, -- JSON array of model signatures - created_at TEXT NOT NULL, - started_at TEXT, - completed_at TEXT, - total_duration_seconds REAL, - error TEXT -); - -CREATE INDEX idx_jobs_status ON jobs(status); -CREATE INDEX idx_jobs_created_at ON jobs(created_at DESC); -``` - -**Table: job_details** -```sql -CREATE TABLE job_details ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - job_id TEXT NOT NULL, - date TEXT NOT NULL, - model TEXT NOT NULL, - status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed')), - started_at TEXT, - completed_at TEXT, - duration_seconds REAL, - error TEXT, - FOREIGN KEY (job_id) REFERENCES jobs(job_id) ON DELETE CASCADE -); - -CREATE INDEX idx_job_details_job_id ON job_details(job_id); -CREATE INDEX idx_job_details_status ON job_details(status); -``` - -### 2.2 Pydantic Models - -**Request Models:** -```python -from pydantic import BaseModel, Field -from typing import Optional, Literal - -class TriggerSimulationRequest(BaseModel): - config_path: Optional[str] = Field(default="configs/default_config.json", description="Path to configuration file") - -class ResultsQueryParams(BaseModel): - date: str = Field(..., pattern=r"^\d{4}-\d{2}-\d{2}$", description="Date in YYYY-MM-DD format") - model: Optional[str] = Field(None, description="Model signature filter") - detail: Literal["minimal", "full"] = Field(default="minimal", description="Response detail level") -``` - -**Response Models:** -```python -class JobProgress(BaseModel): - total_model_days: int - completed: int - failed: int - current: Optional[dict] = None # {"date": str, "model": str} - details: Optional[list] = None # List of JobDetailResponse - -class TriggerSimulationResponse(BaseModel): - job_id: str - status: str - date_range: list[str] - models: list[str] - created_at: str - message: str - progress: Optional[JobProgress] = None - -class JobStatusResponse(BaseModel): - job_id: str - status: str - date_range: list[str] - models: list[str] - progress: JobProgress - created_at: str - updated_at: Optional[str] = None - completed_at: Optional[str] = None - total_duration_seconds: Optional[float] = None - -class DailyPnL(BaseModel): - profit: float - return_pct: float - portfolio_value: float - -class Trade(BaseModel): - id: int - action: str - symbol: str - amount: int - price: Optional[float] = None - total: Optional[float] = None - -class AIReasoning(BaseModel): - total_steps: int - stop_signal_received: bool - reasoning_summary: str - tool_usage: dict[str, int] - -class ModelResult(BaseModel): - model: str - positions: dict[str, float] - daily_pnl: DailyPnL - trades: Optional[list[Trade]] = None - ai_reasoning: Optional[AIReasoning] = None - log_file_path: Optional[str] = None - -class ResultsResponse(BaseModel): - date: str - results: list[ModelResult] -``` - ---- - -## 3. Configuration Management - -### 3.1 Environment Variables - -Required environment variables remain the same as batch mode: -```bash -# OpenAI API Configuration -OPENAI_API_BASE=https://api.openai.com/v1 -OPENAI_API_KEY=sk-... - -# Alpha Vantage API -ALPHAADVANTAGE_API_KEY=... - -# Jina Search API -JINA_API_KEY=... - -# Runtime Config Path (now shared by API and worker) -RUNTIME_ENV_PATH=/app/data/runtime_env.json - -# MCP Service Ports -MATH_HTTP_PORT=8000 -SEARCH_HTTP_PORT=8001 -TRADE_HTTP_PORT=8002 -GETPRICE_HTTP_PORT=8003 - -# API Server Configuration -API_HOST=0.0.0.0 -API_PORT=8080 - -# Job Configuration -MAX_CONCURRENT_JOBS=1 # Only one simulation job at a time -``` - -### 3.2 Runtime State Management - -**Challenge:** Multiple model-days running concurrently need isolated `runtime_env.json` state. - -**Solution:** Per-job runtime config files -- `runtime_env_base.json` - Template -- `runtime_env_{job_id}_{model}_{date}.json` - Job-specific runtime config -- Worker passes custom `RUNTIME_ENV_PATH` to each simulation execution - -**Modified `write_config_value()` and `get_config_value()`:** -- Accept optional `runtime_path` parameter -- Worker manages lifecycle: create โ†’ use โ†’ cleanup - ---- - -## 4. Error Handling - -### 4.1 Error Response Format - -All errors follow this structure: -```json -{ - "error": "error_code", - "message": "Human-readable error description", - "details": { - // Optional additional context - } -} -``` - -### 4.2 HTTP Status Codes - -- `200 OK` - Successful request -- `202 Accepted` - Job queued successfully -- `400 Bad Request` - Invalid input parameters -- `404 Not Found` - Resource not found (job, results) -- `409 Conflict` - Concurrent job conflict -- `500 Internal Server Error` - Unexpected server error -- `503 Service Unavailable` - Health check failed - -### 4.3 Retry Strategy for Workers - -Models run independently - failure of one model doesn't block others: -```python -async def run_model_day(job_id: str, date: str, model_config: dict): - try: - # Execute simulation for this model-day - await agent.run_trading_session(date) - update_job_detail_status(job_id, date, model, "completed") - except Exception as e: - # Log error, update status to failed, continue with next model-day - update_job_detail_status(job_id, date, model, "failed", error=str(e)) - # Do NOT raise - let other models continue -``` - ---- - -## 5. Concurrency & Locking - -### 5.1 Job Execution Policy - -**Rule:** Maximum 1 running job at a time (configurable via `MAX_CONCURRENT_JOBS`) - -**Enforcement:** -```python -def can_start_new_job() -> bool: - running_jobs = db.query( - "SELECT COUNT(*) FROM jobs WHERE status IN ('pending', 'running')" - ).fetchone()[0] - return running_jobs < MAX_CONCURRENT_JOBS -``` - -### 5.2 Position File Concurrency - -**Challenge:** Multiple model-days writing to same model's `position.jsonl` - -**Solution:** Sequential execution per model -```python -# For each date in date_range: -# For each model in parallel: โ† Models run in parallel -# Execute model-day sequentially โ† Dates for same model run sequentially -``` - -**Execution Pattern:** -``` -Date 2025-01-16: - - Model A (running) - - Model B (running) - - Model C (running) - -Date 2025-01-17: โ† Starts only after all models finish 2025-01-16 - - Model A (running) - - Model B (running) - - Model C (running) -``` - -**Rationale:** -- Models write to different position files โ†’ No conflict -- Same model's dates run sequentially โ†’ No race condition on position.jsonl -- Date-level parallelism across models โ†’ Faster overall execution - ---- - -## 6. Performance Considerations - -### 6.1 Execution Time Estimates - -Based on current implementation: -- Single model-day: ~30-60 seconds (depends on AI model latency + tool calls) -- 3 models ร— 5 days = 15 model-days โ‰ˆ 7.5-15 minutes (parallel execution) - -### 6.2 Timeout Configuration - -**API Request Timeout:** -- `/simulate/trigger`: 10 seconds (just queue job) -- `/simulate/status`: 5 seconds (read from DB) -- `/results`: 30 seconds (file I/O + parsing) - -**Worker Timeout:** -- Per model-day: 5 minutes (inherited from `max_retries` ร— `base_delay`) -- Entire job: No timeout (job runs until all model-days complete or fail) - -### 6.3 Optimization Opportunities (Future) - -1. **Results caching:** Store computed daily_pnl in SQLite to avoid recomputation -2. **Parallel date execution:** If position file locking is implemented, run dates in parallel -3. **Streaming responses:** For `/simulate/status`, use SSE to push updates instead of polling - ---- - -## 7. Logging & Observability - -### 7.1 Structured Logging - -All API logs use JSON format: -```json -{ - "timestamp": "2025-01-20T14:30:00Z", - "level": "INFO", - "logger": "api.worker", - "message": "Starting simulation for model-day", - "job_id": "550e8400-...", - "date": "2025-01-16", - "model": "gpt-5" -} -``` - -### 7.2 Log Levels - -- `DEBUG` - Detailed execution flow (tool calls, price fetches) -- `INFO` - Job lifecycle events (created, started, completed) -- `WARNING` - Recoverable errors (retry attempts) -- `ERROR` - Model-day failures (logged but job continues) -- `CRITICAL` - System failures (MCP services down, DB corruption) - -### 7.3 Audit Trail - -All job state transitions logged to `api_audit.log`: -```json -{ - "timestamp": "2025-01-20T14:30:00Z", - "event": "job_created", - "job_id": "550e8400-...", - "user": "windmill-service", // Future: from auth header - "details": {"date_range": [...], "models": [...]} -} -``` - ---- - -## 8. Security Considerations - -### 8.1 Authentication (Future) - -For MVP, API relies on network isolation (Docker network). Future enhancements: -- API key authentication via header: `X-API-Key: ` -- JWT tokens for Windmill integration -- Rate limiting per API key - -### 8.2 Input Validation - -- All date parameters validated with regex: `^\d{4}-\d{2}-\d{2}$` -- Config paths restricted to `configs/` directory (prevent path traversal) -- Model signatures sanitized (alphanumeric + hyphens only) - -### 8.3 File Access Controls - -- Results API only reads from `data/agent_data/` directory -- Config API only reads from `configs/` directory -- No arbitrary file read via API parameters - ---- - -## 9. Deployment Configuration - -### 9.1 Docker Compose - -```yaml -version: '3.8' - -services: - ai-trader-api: - build: - context: . - dockerfile: Dockerfile - ports: - - "8080:8080" - volumes: - - ./data:/app/data - - ./configs:/app/configs - env_file: - - .env - environment: - - MODE=api - - API_PORT=8080 - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - restart: unless-stopped -``` - -### 9.2 Dockerfile Modifications - -```dockerfile -# ... existing layers ... - -# Install API dependencies -COPY requirements-api.txt /app/ -RUN pip install --no-cache-dir -r requirements-api.txt - -# Copy API application code -COPY api/ /app/api/ - -# Copy entrypoint script -COPY docker-entrypoint.sh /app/ -RUN chmod +x /app/docker-entrypoint.sh - -EXPOSE 8080 - -CMD ["/app/docker-entrypoint.sh"] -``` - -### 9.3 Entrypoint Script - -```bash -#!/bin/bash -set -e - -echo "Starting MCP services..." -cd /app/agent_tools -python start_mcp_services.py & -MCP_PID=$! - -echo "Waiting for MCP services to be ready..." -sleep 10 - -echo "Starting API server..." -cd /app -uvicorn api.main:app --host ${API_HOST:-0.0.0.0} --port ${API_PORT:-8080} --workers 1 - -# Cleanup on exit -trap "kill $MCP_PID 2>/dev/null || true" EXIT -``` - ---- - -## 10. API Versioning (Future) - -For v2 and beyond: -- URL prefix: `/api/v1/simulate/trigger`, `/api/v2/simulate/trigger` -- Header-based: `Accept: application/vnd.ai-trader.v1+json` - -MVP uses unversioned endpoints (implied v1). - ---- - -## Next Steps - -After reviewing this specification, we'll proceed to: -1. **Component 2:** Job Manager & SQLite Schema Implementation -2. **Component 3:** Background Worker Architecture -3. **Component 4:** BaseAgent Refactoring for Single-Day Execution -4. **Component 5:** Docker & Deployment Configuration -5. **Component 6:** Windmill Integration Flows - -Please review this API specification and provide feedback or approval to continue. diff --git a/docs/database-enhanced-specification.md b/docs/database-enhanced-specification.md deleted file mode 100644 index e7abcb5..0000000 --- a/docs/database-enhanced-specification.md +++ /dev/null @@ -1,911 +0,0 @@ -# Enhanced Database Specification - Results Storage in SQLite - -## 1. Overview - -**Change from Original Spec:** Instead of reading `position.jsonl` on-demand, simulation results are written to SQLite during execution for faster retrieval and queryability. - -**Benefits:** -- **Faster `/results` endpoint** - No file I/O on every request -- **Advanced querying** - Filter by date range, model, performance metrics -- **Aggregations** - Portfolio timeseries, leaderboards, statistics -- **Data integrity** - Single source of truth with ACID guarantees -- **Backup/restore** - Single database file instead of scattered JSONL files - -**Tradeoff:** Additional database writes during simulation (minimal performance impact) - ---- - -## 2. Enhanced Database Schema - -### 2.1 Complete Table Structure - -```sql --- Job tracking tables (from original spec) -CREATE TABLE IF NOT EXISTS jobs ( - job_id TEXT PRIMARY KEY, - config_path TEXT NOT NULL, - status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'partial', 'failed')), - date_range TEXT NOT NULL, - models TEXT NOT NULL, - created_at TEXT NOT NULL, - started_at TEXT, - completed_at TEXT, - total_duration_seconds REAL, - error TEXT -); - -CREATE TABLE IF NOT EXISTS job_details ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - job_id TEXT NOT NULL, - date TEXT NOT NULL, - model TEXT NOT NULL, - status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed')), - started_at TEXT, - completed_at TEXT, - duration_seconds REAL, - error TEXT, - FOREIGN KEY (job_id) REFERENCES jobs(job_id) ON DELETE CASCADE -); - --- NEW: Simulation results storage -CREATE TABLE IF NOT EXISTS positions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - job_id TEXT NOT NULL, - date TEXT NOT NULL, - model TEXT NOT NULL, - action_id INTEGER NOT NULL, -- Sequence number within that day - action_type TEXT CHECK(action_type IN ('buy', 'sell', 'no_trade')), - symbol TEXT, - amount INTEGER, - price REAL, - cash REAL NOT NULL, - portfolio_value REAL NOT NULL, - daily_profit REAL, - daily_return_pct REAL, - cumulative_profit REAL, - cumulative_return_pct REAL, - created_at TEXT NOT NULL, - FOREIGN KEY (job_id) REFERENCES jobs(job_id) ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS holdings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - position_id INTEGER NOT NULL, - symbol TEXT NOT NULL, - quantity INTEGER NOT NULL, - FOREIGN KEY (position_id) REFERENCES positions(id) ON DELETE CASCADE -); - --- NEW: AI reasoning logs (optional - for detail=full) -CREATE TABLE IF NOT EXISTS reasoning_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - job_id TEXT NOT NULL, - date TEXT NOT NULL, - model TEXT NOT NULL, - step_number INTEGER NOT NULL, - timestamp TEXT NOT NULL, - role TEXT CHECK(role IN ('user', 'assistant', 'tool')), - content TEXT, - tool_name TEXT, - FOREIGN KEY (job_id) REFERENCES jobs(job_id) ON DELETE CASCADE -); - --- NEW: Tool usage statistics -CREATE TABLE IF NOT EXISTS tool_usage ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - job_id TEXT NOT NULL, - date TEXT NOT NULL, - model TEXT NOT NULL, - tool_name TEXT NOT NULL, - call_count INTEGER NOT NULL DEFAULT 1, - total_duration_seconds REAL, - FOREIGN KEY (job_id) REFERENCES jobs(job_id) ON DELETE CASCADE -); - --- Indexes for performance -CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status); -CREATE INDEX IF NOT EXISTS idx_jobs_created_at ON jobs(created_at DESC); -CREATE INDEX IF NOT EXISTS idx_job_details_job_id ON job_details(job_id); -CREATE INDEX IF NOT EXISTS idx_job_details_status ON job_details(status); -CREATE UNIQUE INDEX IF NOT EXISTS idx_job_details_unique ON job_details(job_id, date, model); - -CREATE INDEX IF NOT EXISTS idx_positions_job_id ON positions(job_id); -CREATE INDEX IF NOT EXISTS idx_positions_date ON positions(date); -CREATE INDEX IF NOT EXISTS idx_positions_model ON positions(model); -CREATE INDEX IF NOT EXISTS idx_positions_date_model ON positions(date, model); -CREATE UNIQUE INDEX IF NOT EXISTS idx_positions_unique ON positions(job_id, date, model, action_id); - -CREATE INDEX IF NOT EXISTS idx_holdings_position_id ON holdings(position_id); -CREATE INDEX IF NOT EXISTS idx_holdings_symbol ON holdings(symbol); - -CREATE INDEX IF NOT EXISTS idx_reasoning_logs_job_date_model ON reasoning_logs(job_id, date, model); -CREATE INDEX IF NOT EXISTS idx_tool_usage_job_date_model ON tool_usage(job_id, date, model); -``` - ---- - -### 2.2 Table Relationships - -``` -jobs (1) โ”€โ”€โ”ฌโ”€โ”€> (N) job_details - โ”‚ - โ”œโ”€โ”€> (N) positions โ”€โ”€> (N) holdings - โ”‚ - โ”œโ”€โ”€> (N) reasoning_logs - โ”‚ - โ””โ”€โ”€> (N) tool_usage -``` - ---- - -### 2.3 Data Examples - -#### positions table -``` -id | job_id | date | model | action_id | action_type | symbol | amount | price | cash | portfolio_value | daily_profit | daily_return_pct | cumulative_profit | cumulative_return_pct | created_at ----|------------|------------|-------|-----------|-------------|--------|--------|--------|---------|-----------------|--------------|------------------|-------------------|----------------------|------------ -1 | abc-123... | 2025-01-16 | gpt-5 | 0 | no_trade | NULL | NULL | NULL | 10000.0 | 10000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 2025-01-16T09:30:00Z -2 | abc-123... | 2025-01-16 | gpt-5 | 1 | buy | AAPL | 10 | 255.88 | 7441.2 | 10000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 2025-01-16T09:35:12Z -3 | abc-123... | 2025-01-17 | gpt-5 | 0 | no_trade | NULL | NULL | NULL | 7441.2 | 10150.5 | 150.5 | 1.51 | 150.5 | 1.51 | 2025-01-17T09:30:00Z -4 | abc-123... | 2025-01-17 | gpt-5 | 1 | sell | AAPL | 5 | 262.24 | 8752.4 | 10150.5 | 150.5 | 1.51 | 150.5 | 1.51 | 2025-01-17T09:42:38Z -``` - -#### holdings table -``` -id | position_id | symbol | quantity ----|-------------|--------|---------- -1 | 2 | AAPL | 10 -2 | 3 | AAPL | 10 -3 | 4 | AAPL | 5 -``` - -#### tool_usage table -``` -id | job_id | date | model | tool_name | call_count | total_duration_seconds ----|------------|------------|-------|------------|------------|----------------------- -1 | abc-123... | 2025-01-16 | gpt-5 | get_price | 5 | 2.3 -2 | abc-123... | 2025-01-16 | gpt-5 | search | 3 | 12.7 -3 | abc-123... | 2025-01-16 | gpt-5 | trade | 1 | 0.8 -4 | abc-123... | 2025-01-16 | gpt-5 | math | 2 | 0.1 -``` - ---- - -## 3. Data Migration from position.jsonl - -### 3.1 Migration Strategy - -**During execution:** Write to BOTH SQLite AND position.jsonl for backward compatibility - -**Migration path:** -1. **Phase 1:** Dual-write mode (write to both SQLite and JSONL) -2. **Phase 2:** Verify SQLite data matches JSONL -3. **Phase 3:** Switch `/results` endpoint to read from SQLite -4. **Phase 4:** (Optional) Deprecate JSONL writes - -**Import existing data:** One-time migration script to populate SQLite from existing position.jsonl files - ---- - -### 3.2 Import Script - -```python -# api/import_historical_data.py - -import json -import sqlite3 -from pathlib import Path -from datetime import datetime -from api.database import get_db_connection - -def import_position_jsonl( - model_signature: str, - position_file: Path, - job_id: str = "historical-import" -) -> int: - """ - Import existing position.jsonl data into SQLite. - - Args: - model_signature: Model signature (e.g., "gpt-5") - position_file: Path to position.jsonl - job_id: Job ID to associate with (use "historical-import" for existing data) - - Returns: - Number of records imported - """ - conn = get_db_connection() - cursor = conn.cursor() - - imported_count = 0 - initial_cash = 10000.0 - - with open(position_file, 'r') as f: - for line in f: - if not line.strip(): - continue - - record = json.loads(line) - date = record['date'] - action_id = record['id'] - action = record.get('this_action', {}) - positions = record.get('positions', {}) - - # Extract action details - action_type = action.get('action', 'no_trade') - symbol = action.get('symbol', None) - amount = action.get('amount', None) - price = None # Not stored in original position.jsonl - - # Extract holdings - cash = positions.get('CASH', 0.0) - holdings = {k: v for k, v in positions.items() if k != 'CASH' and v > 0} - - # Calculate portfolio value (approximate - need price data) - portfolio_value = cash # Base value - - # Calculate profits (need previous record) - daily_profit = 0.0 - daily_return_pct = 0.0 - cumulative_profit = cash - initial_cash # Simplified - cumulative_return_pct = (cumulative_profit / initial_cash) * 100 - - # Insert position record - cursor.execute(""" - INSERT INTO positions ( - job_id, date, model, action_id, action_type, symbol, amount, price, - cash, portfolio_value, daily_profit, daily_return_pct, - cumulative_profit, cumulative_return_pct, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - job_id, date, model_signature, action_id, action_type, symbol, amount, price, - cash, portfolio_value, daily_profit, daily_return_pct, - cumulative_profit, cumulative_return_pct, datetime.utcnow().isoformat() + "Z" - )) - - position_id = cursor.lastrowid - - # Insert holdings - for sym, qty in holdings.items(): - cursor.execute(""" - INSERT INTO holdings (position_id, symbol, quantity) - VALUES (?, ?, ?) - """, (position_id, sym, qty)) - - imported_count += 1 - - conn.commit() - conn.close() - - return imported_count - - -def import_all_historical_data(base_path: Path = Path("data/agent_data")) -> dict: - """ - Import all existing position.jsonl files from data/agent_data/. - - Returns: - Summary dict with import counts per model - """ - summary = {} - - for model_dir in base_path.iterdir(): - if not model_dir.is_dir(): - continue - - model_signature = model_dir.name - position_file = model_dir / "position" / "position.jsonl" - - if not position_file.exists(): - continue - - print(f"Importing {model_signature}...") - count = import_position_jsonl(model_signature, position_file) - summary[model_signature] = count - print(f" Imported {count} records") - - return summary - - -if __name__ == "__main__": - print("Starting historical data import...") - summary = import_all_historical_data() - print(f"\nImport complete: {summary}") - print(f"Total records: {sum(summary.values())}") -``` - ---- - -## 4. Updated Results Service - -### 4.1 ResultsService Class - -```python -# api/results_service.py - -from typing import List, Dict, Optional -from datetime import datetime -from api.database import get_db_connection - -class ResultsService: - """ - Service for retrieving simulation results from SQLite. - - Replaces on-demand reading of position.jsonl files. - """ - - def __init__(self, db_path: str = "data/jobs.db"): - self.db_path = db_path - - def get_results( - self, - date: str, - model: Optional[str] = None, - detail: str = "minimal" - ) -> Dict: - """ - Get simulation results for specified date and model(s). - - Args: - date: Trading date (YYYY-MM-DD) - model: Optional model signature filter - detail: "minimal" or "full" - - Returns: - { - "date": str, - "results": [ - { - "model": str, - "positions": {...}, - "daily_pnl": {...}, - "trades": [...], // if detail=full - "ai_reasoning": {...} // if detail=full - } - ] - } - """ - conn = get_db_connection(self.db_path) - - # Get all models for this date (or specific model) - if model: - models = [model] - else: - cursor = conn.cursor() - cursor.execute(""" - SELECT DISTINCT model FROM positions WHERE date = ? - """, (date,)) - models = [row[0] for row in cursor.fetchall()] - - results = [] - - for mdl in models: - result = self._get_model_result(conn, date, mdl, detail) - if result: - results.append(result) - - conn.close() - - return { - "date": date, - "results": results - } - - def _get_model_result( - self, - conn, - date: str, - model: str, - detail: str - ) -> Optional[Dict]: - """Get result for single model on single date""" - cursor = conn.cursor() - - # Get latest position for this date (highest action_id) - cursor.execute(""" - SELECT - cash, portfolio_value, daily_profit, daily_return_pct, - cumulative_profit, cumulative_return_pct - FROM positions - WHERE date = ? AND model = ? - ORDER BY action_id DESC - LIMIT 1 - """, (date, model)) - - row = cursor.fetchone() - if not row: - return None - - cash, portfolio_value, daily_profit, daily_return_pct, cumulative_profit, cumulative_return_pct = row - - # Get holdings for latest position - cursor.execute(""" - SELECT h.symbol, h.quantity - FROM holdings h - JOIN positions p ON h.position_id = p.id - WHERE p.date = ? AND p.model = ? - ORDER BY p.action_id DESC - LIMIT 100 -- One position worth of holdings - """, (date, model)) - - holdings = {row[0]: row[1] for row in cursor.fetchall()} - holdings['CASH'] = cash - - result = { - "model": model, - "positions": holdings, - "daily_pnl": { - "profit": daily_profit, - "return_pct": daily_return_pct, - "portfolio_value": portfolio_value - }, - "cumulative_pnl": { - "profit": cumulative_profit, - "return_pct": cumulative_return_pct - } - } - - # Add full details if requested - if detail == "full": - result["trades"] = self._get_trades(cursor, date, model) - result["ai_reasoning"] = self._get_reasoning(cursor, date, model) - result["tool_usage"] = self._get_tool_usage(cursor, date, model) - - return result - - def _get_trades(self, cursor, date: str, model: str) -> List[Dict]: - """Get all trades executed on this date""" - cursor.execute(""" - SELECT action_id, action_type, symbol, amount, price - FROM positions - WHERE date = ? AND model = ? AND action_type IN ('buy', 'sell') - ORDER BY action_id - """, (date, model)) - - trades = [] - for row in cursor.fetchall(): - trades.append({ - "id": row[0], - "action": row[1], - "symbol": row[2], - "amount": row[3], - "price": row[4], - "total": row[3] * row[4] if row[3] and row[4] else None - }) - - return trades - - def _get_reasoning(self, cursor, date: str, model: str) -> Dict: - """Get AI reasoning summary""" - cursor.execute(""" - SELECT COUNT(*) as total_steps, - COUNT(CASE WHEN role = 'assistant' THEN 1 END) as assistant_messages, - COUNT(CASE WHEN role = 'tool' THEN 1 END) as tool_messages - FROM reasoning_logs - WHERE date = ? AND model = ? - """, (date, model)) - - row = cursor.fetchone() - total_steps = row[0] if row else 0 - - # Get reasoning summary (last assistant message with FINISH_SIGNAL) - cursor.execute(""" - SELECT content FROM reasoning_logs - WHERE date = ? AND model = ? AND role = 'assistant' - AND content LIKE '%%' - ORDER BY step_number DESC - LIMIT 1 - """, (date, model)) - - row = cursor.fetchone() - reasoning_summary = row[0] if row else "No reasoning summary available" - - return { - "total_steps": total_steps, - "stop_signal_received": "" in reasoning_summary, - "reasoning_summary": reasoning_summary[:500] # Truncate for brevity - } - - def _get_tool_usage(self, cursor, date: str, model: str) -> Dict[str, int]: - """Get tool usage counts""" - cursor.execute(""" - SELECT tool_name, call_count - FROM tool_usage - WHERE date = ? AND model = ? - """, (date, model)) - - return {row[0]: row[1] for row in cursor.fetchall()} - - def get_portfolio_timeseries( - self, - model: str, - start_date: Optional[str] = None, - end_date: Optional[str] = None - ) -> List[Dict]: - """ - Get portfolio value over time for a model. - - Returns: - [ - {"date": "2025-01-16", "portfolio_value": 10000.0, "daily_return_pct": 0.0}, - {"date": "2025-01-17", "portfolio_value": 10150.5, "daily_return_pct": 1.51}, - ... - ] - """ - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - query = """ - SELECT date, portfolio_value, daily_return_pct, cumulative_return_pct - FROM ( - SELECT date, portfolio_value, daily_return_pct, cumulative_return_pct, - ROW_NUMBER() OVER (PARTITION BY date ORDER BY action_id DESC) as rn - FROM positions - WHERE model = ? - ) - WHERE rn = 1 - """ - - params = [model] - - if start_date: - query += " AND date >= ?" - params.append(start_date) - if end_date: - query += " AND date <= ?" - params.append(end_date) - - query += " ORDER BY date ASC" - - cursor.execute(query, params) - - timeseries = [] - for row in cursor.fetchall(): - timeseries.append({ - "date": row[0], - "portfolio_value": row[1], - "daily_return_pct": row[2], - "cumulative_return_pct": row[3] - }) - - conn.close() - return timeseries - - def get_leaderboard(self, date: Optional[str] = None) -> List[Dict]: - """ - Get model performance leaderboard. - - Args: - date: Optional date filter (latest results if not specified) - - Returns: - [ - {"model": "gpt-5", "portfolio_value": 10500, "cumulative_return_pct": 5.0, "rank": 1}, - {"model": "claude-3.7-sonnet", "portfolio_value": 10300, "cumulative_return_pct": 3.0, "rank": 2}, - ... - ] - """ - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - if date: - # Specific date leaderboard - cursor.execute(""" - SELECT model, portfolio_value, cumulative_return_pct - FROM ( - SELECT model, portfolio_value, cumulative_return_pct, - ROW_NUMBER() OVER (PARTITION BY model ORDER BY action_id DESC) as rn - FROM positions - WHERE date = ? - ) - WHERE rn = 1 - ORDER BY portfolio_value DESC - """, (date,)) - else: - # Latest results for each model - cursor.execute(""" - SELECT model, portfolio_value, cumulative_return_pct - FROM ( - SELECT model, portfolio_value, cumulative_return_pct, - ROW_NUMBER() OVER (PARTITION BY model ORDER BY date DESC, action_id DESC) as rn - FROM positions - ) - WHERE rn = 1 - ORDER BY portfolio_value DESC - """) - - leaderboard = [] - rank = 1 - for row in cursor.fetchall(): - leaderboard.append({ - "rank": rank, - "model": row[0], - "portfolio_value": row[1], - "cumulative_return_pct": row[2] - }) - rank += 1 - - conn.close() - return leaderboard -``` - ---- - -## 5. Updated Executor - Write to SQLite - -```python -# api/executor.py (additions to existing code) - -class ModelDayExecutor: - # ... existing code ... - - async def run_model_day( - self, - job_id: str, - date: str, - model_config: Dict[str, Any], - agent_class: type, - config: Dict[str, Any] - ) -> None: - """Execute simulation for one model on one date""" - - # ... existing execution code ... - - try: - # Execute trading session - await agent.run_trading_session(date) - - # NEW: Extract and store results in SQLite - self._store_results_to_db(job_id, date, model_sig) - - # Mark as completed - self.job_manager.update_job_detail_status( - job_id, date, model_sig, "completed" - ) - - except Exception as e: - # ... error handling ... - - def _store_results_to_db(self, job_id: str, date: str, model: str) -> None: - """ - Extract data from position.jsonl and log.jsonl, store in SQLite. - - This runs after agent.run_trading_session() completes. - """ - from api.database import get_db_connection - from pathlib import Path - import json - - conn = get_db_connection() - cursor = conn.cursor() - - # Read position.jsonl for this model - position_file = Path(f"data/agent_data/{model}/position/position.jsonl") - - if not position_file.exists(): - logger.warning(f"Position file not found: {position_file}") - return - - # Find records for this date - with open(position_file, 'r') as f: - for line in f: - if not line.strip(): - continue - - record = json.loads(line) - if record['date'] != date: - continue # Skip other dates - - # Extract fields - action_id = record['id'] - action = record.get('this_action', {}) - positions = record.get('positions', {}) - - action_type = action.get('action', 'no_trade') - symbol = action.get('symbol') - amount = action.get('amount') - price = None # TODO: Get from price data if needed - - cash = positions.get('CASH', 0.0) - holdings = {k: v for k, v in positions.items() if k != 'CASH' and v > 0} - - # Calculate portfolio value (simplified - improve with actual prices) - portfolio_value = cash # + sum(holdings value) - - # Calculate daily P&L (compare to previous day's closing value) - # TODO: Implement proper P&L calculation - - # Insert position - cursor.execute(""" - INSERT INTO positions ( - job_id, date, model, action_id, action_type, symbol, amount, price, - cash, portfolio_value, daily_profit, daily_return_pct, - cumulative_profit, cumulative_return_pct, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - job_id, date, model, action_id, action_type, symbol, amount, price, - cash, portfolio_value, 0.0, 0.0, # TODO: Calculate P&L - 0.0, 0.0, # TODO: Calculate cumulative P&L - datetime.utcnow().isoformat() + "Z" - )) - - position_id = cursor.lastrowid - - # Insert holdings - for sym, qty in holdings.items(): - cursor.execute(""" - INSERT INTO holdings (position_id, symbol, quantity) - VALUES (?, ?, ?) - """, (position_id, sym, qty)) - - # Parse log.jsonl for reasoning (if detail=full is needed later) - # TODO: Implement log parsing and storage in reasoning_logs table - - conn.commit() - conn.close() - - logger.info(f"Stored results for {model} on {date} in SQLite") -``` - ---- - -## 6. Migration Path - -### 6.1 Backward Compatibility - -**Keep position.jsonl writes** to ensure existing tools/scripts continue working: - -```python -# In agent/base_agent/base_agent.py - no changes needed -# position.jsonl writing continues as normal - -# In api/executor.py - AFTER position.jsonl is written -await agent.run_trading_session(date) # Writes to position.jsonl -self._store_results_to_db(job_id, date, model_sig) # Copies to SQLite -``` - -### 6.2 Gradual Migration - -**Week 1:** Deploy with dual-write (JSONL + SQLite) -**Week 2:** Verify data consistency, fix any discrepancies -**Week 3:** Switch `/results` endpoint to read from SQLite -**Week 4:** (Optional) Remove JSONL writes - ---- - -## 7. Updated API Endpoints - -### 7.1 Enhanced `/results` Endpoint - -```python -# api/main.py - -from api.results_service import ResultsService - -results_service = ResultsService() - -@app.get("/results") -async def get_results( - date: str, - model: Optional[str] = None, - detail: str = "minimal" -): - """Get simulation results from SQLite (fast!)""" - # Validate date format - try: - datetime.strptime(date, "%Y-%m-%d") - except ValueError: - raise HTTPException(status_code=400, detail="Invalid date format (use YYYY-MM-DD)") - - results = results_service.get_results(date, model, detail) - - if not results["results"]: - raise HTTPException(status_code=404, detail=f"No data found for date {date}") - - return results -``` - -### 7.2 New Endpoints for Advanced Queries - -```python -@app.get("/portfolio/timeseries") -async def get_portfolio_timeseries( - model: str, - start_date: Optional[str] = None, - end_date: Optional[str] = None -): - """Get portfolio value over time for a model""" - timeseries = results_service.get_portfolio_timeseries(model, start_date, end_date) - - if not timeseries: - raise HTTPException(status_code=404, detail=f"No data found for model {model}") - - return { - "model": model, - "timeseries": timeseries - } - - -@app.get("/leaderboard") -async def get_leaderboard(date: Optional[str] = None): - """Get model performance leaderboard""" - leaderboard = results_service.get_leaderboard(date) - - return { - "date": date or "latest", - "leaderboard": leaderboard - } -``` - ---- - -## 8. Database Maintenance - -### 8.1 Cleanup Old Data - -```python -# api/job_manager.py (add method) - -def cleanup_old_data(self, days: int = 90) -> dict: - """ - Delete jobs and associated data older than specified days. - - Returns: - Summary of deleted records - """ - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - cutoff_date = (datetime.utcnow() - timedelta(days=days)).isoformat() + "Z" - - # Count records before deletion - cursor.execute("SELECT COUNT(*) FROM jobs WHERE created_at < ?", (cutoff_date,)) - jobs_to_delete = cursor.fetchone()[0] - - cursor.execute(""" - SELECT COUNT(*) FROM positions - WHERE job_id IN (SELECT job_id FROM jobs WHERE created_at < ?) - """, (cutoff_date,)) - positions_to_delete = cursor.fetchone()[0] - - # Delete (CASCADE will handle related tables) - cursor.execute("DELETE FROM jobs WHERE created_at < ?", (cutoff_date,)) - - conn.commit() - conn.close() - - return { - "cutoff_date": cutoff_date, - "jobs_deleted": jobs_to_delete, - "positions_deleted": positions_to_delete - } -``` - -### 8.2 Vacuum Database - -```python -def vacuum_database(self) -> None: - """Reclaim disk space after deletes""" - conn = get_db_connection(self.db_path) - conn.execute("VACUUM") - conn.close() -``` - ---- - -## Summary - -**Enhanced database schema** with 6 tables: -- `jobs`, `job_details` (job tracking) -- `positions`, `holdings` (simulation results) -- `reasoning_logs`, `tool_usage` (AI details) - -**Benefits:** -- โšก **10-100x faster** `/results` queries (no file I/O) -- ๐Ÿ“Š **Advanced analytics** - timeseries, leaderboards, aggregations -- ๐Ÿ”’ **Data integrity** - ACID compliance, foreign keys -- ๐Ÿ—„๏ธ **Single source of truth** - all data in one place - -**Migration strategy:** Dual-write (JSONL + SQLite) for backward compatibility - -**Next:** Comprehensive testing suite specification diff --git a/docs/deployment/docker-deployment.md b/docs/deployment/docker-deployment.md new file mode 100644 index 0000000..a820b33 --- /dev/null +++ b/docs/deployment/docker-deployment.md @@ -0,0 +1,95 @@ +# Docker Deployment + +Production Docker deployment guide. + +--- + +## Quick Deployment + +```bash +git clone https://github.com/Xe138/AI-Trader.git +cd AI-Trader +cp .env.example .env +# Edit .env with API keys +docker-compose up -d +``` + +--- + +## Production Configuration + +### Use Pre-built Image + +```yaml +# docker-compose.yml +services: + ai-trader: + image: ghcr.io/xe138/ai-trader:latest + # ... rest of config +``` + +### Build Locally + +```yaml +# docker-compose.yml +services: + ai-trader: + build: . + # ... rest of config +``` + +--- + +## Volume Persistence + +Ensure data persists across restarts: + +```yaml +volumes: + - ./data:/app/data # Required: database and cache + - ./logs:/app/logs # Recommended: application logs + - ./configs:/app/configs # Required: model configurations +``` + +--- + +## Environment Security + +- Never commit `.env` to version control +- Use secrets management (Docker secrets, Kubernetes secrets) +- Rotate API keys regularly +- Restrict network access to API port + +--- + +## Health Checks + +Docker automatically restarts unhealthy containers: + +```yaml +healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s +``` + +--- + +## Monitoring + +```bash +# Container status +docker ps + +# Resource usage +docker stats ai-trader + +# Logs +docker logs -f ai-trader +``` + +--- + +See [DOCKER_API.md](../../DOCKER_API.md) for detailed Docker documentation. diff --git a/docs/deployment/monitoring.md b/docs/deployment/monitoring.md new file mode 100644 index 0000000..42fb104 --- /dev/null +++ b/docs/deployment/monitoring.md @@ -0,0 +1,49 @@ +# Monitoring + +Health checks, logging, and metrics. + +--- + +## Health Checks + +```bash +# Manual check +curl http://localhost:8080/health + +# Automated monitoring (cron) +*/5 * * * * curl -f http://localhost:8080/health || echo "API down" | mail -s "Alert" admin@example.com +``` + +--- + +## Logging + +```bash +# View logs +docker logs -f ai-trader + +# Filter errors +docker logs ai-trader 2>&1 | grep -i error + +# Export logs +docker logs ai-trader > ai-trader.log 2>&1 +``` + +--- + +## Database Monitoring + +```bash +# Database size +docker exec ai-trader du -h /app/data/jobs.db + +# Job statistics +docker exec ai-trader sqlite3 /app/data/jobs.db \ + "SELECT status, COUNT(*) FROM jobs GROUP BY status;" +``` + +--- + +## Metrics (Future) + +Prometheus metrics planned for v0.4.0. diff --git a/docs/deployment/production-checklist.md b/docs/deployment/production-checklist.md new file mode 100644 index 0000000..5c4de6c --- /dev/null +++ b/docs/deployment/production-checklist.md @@ -0,0 +1,50 @@ +# Production Deployment Checklist + +Pre-deployment verification. + +--- + +## Pre-Deployment + +- [ ] API keys configured in `.env` +- [ ] Environment variables reviewed +- [ ] Model configuration validated +- [ ] Port availability confirmed +- [ ] Volume mounts configured +- [ ] Health checks enabled +- [ ] Restart policy set + +--- + +## Testing + +- [ ] `bash scripts/validate_docker_build.sh` passes +- [ ] `bash scripts/test_api_endpoints.sh` passes +- [ ] Health endpoint responds correctly +- [ ] Sample simulation completes successfully + +--- + +## Monitoring + +- [ ] Log aggregation configured +- [ ] Health check monitoring enabled +- [ ] Alerting configured for failures +- [ ] Database backup strategy defined + +--- + +## Security + +- [ ] API keys stored securely (not in code) +- [ ] `.env` excluded from version control +- [ ] Network access restricted +- [ ] SSL/TLS configured (if exposing publicly) + +--- + +## Documentation + +- [ ] Runbook created for operations team +- [ ] Escalation procedures documented +- [ ] Recovery procedures tested diff --git a/docs/deployment/scaling.md b/docs/deployment/scaling.md new file mode 100644 index 0000000..3fe5d57 --- /dev/null +++ b/docs/deployment/scaling.md @@ -0,0 +1,46 @@ +# Scaling + +Running multiple instances and load balancing. + +--- + +## Current Limitations + +- Maximum 1 concurrent job per instance +- No built-in load balancing +- Single SQLite database per instance + +--- + +## Multi-Instance Deployment + +For parallel simulations, deploy multiple instances: + +```yaml +# docker-compose.yml +services: + ai-trader-1: + image: ghcr.io/xe138/ai-trader:latest + ports: + - "8081:8080" + volumes: + - ./data1:/app/data + + ai-trader-2: + image: ghcr.io/xe138/ai-trader:latest + ports: + - "8082:8080" + volumes: + - ./data2:/app/data +``` + +**Note:** Each instance needs separate database and data volumes. + +--- + +## Load Balancing (Future) + +Planned for v0.4.0: +- Shared PostgreSQL database +- Job queue with multiple workers +- Horizontal scaling support diff --git a/docs/developer/CONTRIBUTING.md b/docs/developer/CONTRIBUTING.md new file mode 100644 index 0000000..a40ded4 --- /dev/null +++ b/docs/developer/CONTRIBUTING.md @@ -0,0 +1,48 @@ +# Contributing to AI-Trader + +Guidelines for contributing to the project. + +--- + +## Development Setup + +See [development-setup.md](development-setup.md) + +--- + +## Pull Request Process + +1. Fork the repository +2. Create feature branch: `git checkout -b feature/my-feature` +3. Make changes +4. Run tests: `pytest tests/` +5. Update documentation +6. Commit: `git commit -m "Add feature: description"` +7. Push: `git push origin feature/my-feature` +8. Create Pull Request + +--- + +## Code Style + +- Follow PEP 8 for Python +- Use type hints +- Add docstrings to public functions +- Keep functions focused and small + +--- + +## Testing Requirements + +- Unit tests for new functionality +- Integration tests for API changes +- Maintain test coverage >80% + +--- + +## Documentation + +- Update README.md for new features +- Add entries to CHANGELOG.md +- Update API_REFERENCE.md for endpoint changes +- Include examples in relevant guides diff --git a/docs/developer/adding-models.md b/docs/developer/adding-models.md new file mode 100644 index 0000000..b662ffd --- /dev/null +++ b/docs/developer/adding-models.md @@ -0,0 +1,69 @@ +# Adding Custom AI Models + +How to add and configure custom AI models. + +--- + +## Basic Setup + +Edit `configs/default_config.json`: + +```json +{ + "models": [ + { + "name": "Your Model Name", + "basemodel": "provider/model-id", + "signature": "unique-identifier", + "enabled": true + } + ] +} +``` + +--- + +## Examples + +### OpenAI Models + +```json +{ + "name": "GPT-4", + "basemodel": "openai/gpt-4", + "signature": "gpt-4", + "enabled": true +} +``` + +### Anthropic Claude + +```json +{ + "name": "Claude 3.7 Sonnet", + "basemodel": "anthropic/claude-3.7-sonnet", + "signature": "claude-3.7-sonnet", + "enabled": true, + "openai_base_url": "https://api.anthropic.com/v1", + "openai_api_key": "your-anthropic-key" +} +``` + +### Via OpenRouter + +```json +{ + "name": "DeepSeek", + "basemodel": "deepseek/deepseek-chat", + "signature": "deepseek", + "enabled": true, + "openai_base_url": "https://openrouter.ai/api/v1", + "openai_api_key": "your-openrouter-key" +} +``` + +--- + +## Field Reference + +See [docs/user-guide/configuration.md](../user-guide/configuration.md#model-configuration-fields) for complete field descriptions. diff --git a/docs/developer/architecture.md b/docs/developer/architecture.md new file mode 100644 index 0000000..7940e87 --- /dev/null +++ b/docs/developer/architecture.md @@ -0,0 +1,68 @@ +# Architecture + +System design and component overview. + +--- + +## Component Diagram + +See README.md for architecture diagram. + +--- + +## Key Components + +### FastAPI Server (`api/main.py`) +- REST API endpoints +- Request validation +- Response formatting + +### Job Manager (`api/job_manager.py`) +- Job lifecycle management +- SQLite operations +- Concurrency control + +### Simulation Worker (`api/simulation_worker.py`) +- Background job execution +- Date-sequential, model-parallel orchestration +- Error handling + +### Model-Day Executor (`api/model_day_executor.py`) +- Single model-day execution +- Runtime config isolation +- Agent invocation + +### Base Agent (`agent/base_agent/base_agent.py`) +- Trading session execution +- MCP tool integration +- Position management + +### MCP Services (`agent_tools/`) +- Math, Search, Trade, Price tools +- Internal HTTP servers +- Localhost-only access + +--- + +## Data Flow + +1. API receives trigger request +2. Job Manager validates and creates job +3. Worker starts background execution +4. For each date (sequential): + - For each model (parallel): + - Executor creates isolated runtime config + - Agent executes trading session + - Results stored in database +5. Job status updated +6. Results available via API + +--- + +## Anti-Look-Ahead Controls + +- `TODAY_DATE` in runtime config limits data access +- Price queries filter by date +- Search results filtered by publication date + +See [CLAUDE.md](../../CLAUDE.md) for implementation details. diff --git a/docs/developer/database-schema.md b/docs/developer/database-schema.md new file mode 100644 index 0000000..e1d6676 --- /dev/null +++ b/docs/developer/database-schema.md @@ -0,0 +1,94 @@ +# Database Schema + +SQLite database schema reference. + +--- + +## Tables + +### jobs +Job metadata and overall status. + +```sql +CREATE TABLE jobs ( + job_id TEXT PRIMARY KEY, + config_path TEXT NOT NULL, + status TEXT CHECK(status IN ('pending', 'running', 'completed', 'partial', 'failed')), + date_range TEXT, -- JSON array + models TEXT, -- JSON array + created_at TEXT, + started_at TEXT, + completed_at TEXT, + total_duration_seconds REAL, + error TEXT +); +``` + +### job_details +Per model-day execution details. + +```sql +CREATE TABLE job_details ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + job_id TEXT, + model_signature TEXT, + trading_date TEXT, + status TEXT CHECK(status IN ('pending', 'running', 'completed', 'failed')), + start_time TEXT, + end_time TEXT, + duration_seconds REAL, + error TEXT, + FOREIGN KEY (job_id) REFERENCES jobs(job_id) ON DELETE CASCADE +); +``` + +### positions +Trading position records with P&L. + +```sql +CREATE TABLE positions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + job_id TEXT, + date TEXT, + model TEXT, + action_id INTEGER, + action_type TEXT, + symbol TEXT, + amount INTEGER, + price REAL, + cash REAL, + portfolio_value REAL, + daily_profit REAL, + daily_return_pct REAL, + created_at TEXT +); +``` + +### holdings +Portfolio holdings breakdown per position. + +```sql +CREATE TABLE holdings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + position_id INTEGER, + symbol TEXT, + quantity REAL, + FOREIGN KEY (position_id) REFERENCES positions(id) ON DELETE CASCADE +); +``` + +### price_data +Cached historical price data. + +### price_coverage +Data availability tracking per symbol. + +### reasoning_logs +AI decision reasoning (when enabled). + +### tool_usage +MCP tool usage statistics. + +--- + +See `api/database.py` for complete schema definitions. diff --git a/docs/developer/development-setup.md b/docs/developer/development-setup.md new file mode 100644 index 0000000..6901038 --- /dev/null +++ b/docs/developer/development-setup.md @@ -0,0 +1,71 @@ +# Development Setup + +Local development without Docker. + +--- + +## Prerequisites + +- Python 3.10+ +- pip +- virtualenv + +--- + +## Setup Steps + +### 1. Clone Repository + +```bash +git clone https://github.com/Xe138/AI-Trader.git +cd AI-Trader +``` + +### 2. Create Virtual Environment + +```bash +python3 -m venv venv +source venv/bin/activate # Linux/Mac +# venv\Scripts\activate # Windows +``` + +### 3. Install Dependencies + +```bash +pip install -r requirements.txt +``` + +### 4. Configure Environment + +```bash +cp .env.example .env +# Edit .env with your API keys +``` + +### 5. Start MCP Services + +```bash +cd agent_tools +python start_mcp_services.py & +cd .. +``` + +### 6. Start API Server + +```bash +python -m uvicorn api.main:app --reload --port 8080 +``` + +--- + +## Running Tests + +```bash +pytest tests/ -v +``` + +--- + +## Project Structure + +See [CLAUDE.md](../../CLAUDE.md) for complete project structure. diff --git a/docs/developer/testing.md b/docs/developer/testing.md new file mode 100644 index 0000000..56e091b --- /dev/null +++ b/docs/developer/testing.md @@ -0,0 +1,64 @@ +# Testing Guide + +Guide for testing AI-Trader during development. + +--- + +## Automated Testing + +### Docker Build Validation + +```bash +chmod +x scripts/*.sh +bash scripts/validate_docker_build.sh +``` + +Validates: +- Docker installation +- Environment configuration +- Image build +- Container startup +- Health endpoint + +### API Endpoint Testing + +```bash +bash scripts/test_api_endpoints.sh +``` + +Tests all API endpoints with real simulations. + +--- + +## Unit Tests + +```bash +# Install dependencies +pip install -r requirements.txt + +# Run tests +pytest tests/ -v + +# With coverage +pytest tests/ -v --cov=api --cov-report=term-missing + +# Specific test file +pytest tests/unit/test_job_manager.py -v +``` + +--- + +## Integration Tests + +```bash +# Run integration tests only +pytest tests/integration/ -v + +# Test with real API server +docker-compose up -d +pytest tests/integration/test_api_endpoints.py -v +``` + +--- + +For detailed testing procedures, see root [TESTING_GUIDE.md](../../TESTING_GUIDE.md). diff --git a/docs/implementation-specifications.md b/docs/implementation-specifications.md deleted file mode 100644 index d1394d8..0000000 --- a/docs/implementation-specifications.md +++ /dev/null @@ -1,873 +0,0 @@ -# Implementation Specifications: Agent, Docker, and Windmill Integration - -## Part 1: BaseAgent Refactoring - -### 1.1 Current State Analysis - -**Current `base_agent.py` structure:** -- `run_date_range(init_date, end_date)` - Loops through all dates -- `run_trading_session(today_date)` - Executes single day -- `get_trading_dates()` - Calculates dates from position.jsonl - -**What works well:** -- `run_trading_session()` is already isolated for single-day execution โœ… -- Agent initialization is separate from execution โœ… -- Position tracking via position.jsonl โœ… - -**What needs modification:** -- `runtime_env.json` management (move to RuntimeConfigManager) -- `get_trading_dates()` logic (move to API layer for date range calculation) - -### 1.2 Required Changes - -#### Change 1: No modifications needed to core execution logic - -**Rationale:** `BaseAgent.run_trading_session(today_date)` already supports single-day execution. The worker will call this method directly. - -```python -# Current code (already suitable for API mode): -async def run_trading_session(self, today_date: str) -> None: - """Run single day trading session""" - # This method is perfect as-is for worker to call -``` - -**Action:** โœ… No changes needed - ---- - -#### Change 2: Make runtime config path injectable - -**Current issue:** -```python -# In base_agent.py, uses global config -from tools.general_tools import get_config_value, write_config_value -``` - -**Problem:** `get_config_value()` reads from `os.environ["RUNTIME_ENV_PATH"]`, which the worker will override per execution. - -**Solution:** Already works! The worker sets `RUNTIME_ENV_PATH` before calling agent methods: - -```python -# In executor.py -os.environ["RUNTIME_ENV_PATH"] = runtime_config_path -await agent.run_trading_session(date) -``` - -**Action:** โœ… No changes needed (env var override is sufficient) - ---- - -#### Change 3: Optional - Separate agent initialization from date-range logic - -**Current code in `main.py`:** -```python -# Creates agent -agent = AgentClass(...) -await agent.initialize() - -# Runs all dates -await agent.run_date_range(INIT_DATE, END_DATE) -``` - -**For API mode:** -```python -# Worker creates agent -agent = AgentClass(...) -await agent.initialize() - -# Worker calls run_trading_session directly for each date -for date in date_range: - await agent.run_trading_session(date) -``` - -**Action:** โœ… Worker will not use `run_date_range()` method. No changes needed to agent. - ---- - -### 1.3 Summary: BaseAgent Changes - -**Result:** **NO CODE CHANGES REQUIRED** to `base_agent.py`! - -The existing architecture is already compatible with the API worker pattern: -- `run_trading_session()` is the perfect interface -- Runtime config is managed via environment variables -- Position tracking works as-is - -**Only change needed:** Worker must call `agent.register_agent()` if position file doesn't exist (already handled by `get_trading_dates()` logic). - ---- - -## Part 2: Docker Configuration - -### 2.1 Current Docker Setup - -**Existing files:** -- `Dockerfile` - Multi-stage build for batch mode -- `docker-compose.yml` - Service definition -- `docker-entrypoint.sh` - Launches data fetch + main.py - -### 2.2 Modified Dockerfile - -```dockerfile -# Existing stages remain the same... -FROM python:3.10-slim - -WORKDIR /app - -# Install system dependencies -RUN apt-get update && apt-get install -y \ - curl \ - && rm -rf /var/lib/apt/lists/* - -# Copy requirements -COPY requirements.txt requirements-api.txt ./ -RUN pip install --no-cache-dir -r requirements.txt -RUN pip install --no-cache-dir -r requirements-api.txt - -# Copy application code -COPY . /app - -# Create data directories -RUN mkdir -p /app/data /app/configs - -# Copy and set permissions for entrypoint -COPY docker-entrypoint-api.sh /app/ -RUN chmod +x /app/docker-entrypoint-api.sh - -# Expose API port -EXPOSE 8080 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD curl -f http://localhost:8080/health || exit 1 - -# Run API service -CMD ["/app/docker-entrypoint-api.sh"] -``` - -### 2.3 New requirements-api.txt - -``` -fastapi==0.109.0 -uvicorn[standard]==0.27.0 -pydantic==2.5.3 -pydantic-settings==2.1.0 -python-multipart==0.0.6 -``` - -### 2.4 New docker-entrypoint-api.sh - -```bash -#!/bin/bash -set -e - -echo "==================================" -echo "AI-Trader API Service Starting" -echo "==================================" - -# Cleanup stale runtime configs from previous runs -echo "Cleaning up stale runtime configs..." -python3 -c "from api.runtime_manager import RuntimeConfigManager; RuntimeConfigManager().cleanup_all_runtime_configs()" - -# Start MCP services in background -echo "Starting MCP services..." -cd /app/agent_tools -python3 start_mcp_services.py & -MCP_PID=$! - -# Wait for MCP services to be ready -echo "Waiting for MCP services to initialize..." -sleep 10 - -# Verify MCP services are running -echo "Verifying MCP services..." -for port in ${MATH_HTTP_PORT:-8000} ${SEARCH_HTTP_PORT:-8001} ${TRADE_HTTP_PORT:-8002} ${GETPRICE_HTTP_PORT:-8003}; do - if ! curl -f -s http://localhost:$port/health > /dev/null 2>&1; then - echo "WARNING: MCP service on port $port not responding" - else - echo "โœ“ MCP service on port $port is healthy" - fi -done - -# Start API server -echo "Starting FastAPI server..." -cd /app - -# Use environment variables for host and port -API_HOST=${API_HOST:-0.0.0.0} -API_PORT=${API_PORT:-8080} - -echo "API will be available at http://${API_HOST}:${API_PORT}" -echo "==================================" - -# Start uvicorn with single worker (for simplicity in MVP) -exec uvicorn api.main:app \ - --host ${API_HOST} \ - --port ${API_PORT} \ - --workers 1 \ - --log-level info - -# Cleanup function (called on exit) -trap "echo 'Shutting down...'; kill $MCP_PID 2>/dev/null || true" EXIT SIGTERM SIGINT -``` - -### 2.5 Updated docker-compose.yml - -```yaml -version: '3.8' - -services: - ai-trader: - build: - context: . - dockerfile: Dockerfile - container_name: ai-trader-api - ports: - - "8080:8080" - volumes: - - ./data:/app/data - - ./configs:/app/configs - - ./logs:/app/logs - env_file: - - .env - environment: - - API_HOST=0.0.0.0 - - API_PORT=8080 - - RUNTIME_ENV_PATH=/app/data/runtime_env.json - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - restart: unless-stopped - networks: - - ai-trader-network - -networks: - ai-trader-network: - driver: bridge -``` - -### 2.6 Environment Variables Reference - -```bash -# .env file example for API mode - -# OpenAI Configuration -OPENAI_API_BASE=https://api.openai.com/v1 -OPENAI_API_KEY=sk-... - -# API Keys -ALPHAADVANTAGE_API_KEY=your_alpha_vantage_key -JINA_API_KEY=your_jina_key - -# MCP Service Ports -MATH_HTTP_PORT=8000 -SEARCH_HTTP_PORT=8001 -TRADE_HTTP_PORT=8002 -GETPRICE_HTTP_PORT=8003 - -# API Configuration -API_HOST=0.0.0.0 -API_PORT=8080 - -# Runtime Config -RUNTIME_ENV_PATH=/app/data/runtime_env.json - -# Job Configuration -MAX_CONCURRENT_JOBS=1 -``` - -### 2.7 Docker Commands Reference - -```bash -# Build image -docker-compose build - -# Start service -docker-compose up - -# Start in background -docker-compose up -d - -# View logs -docker-compose logs -f - -# Check health -docker-compose ps - -# Stop service -docker-compose down - -# Restart service -docker-compose restart - -# Execute command in running container -docker-compose exec ai-trader python3 -c "from api.job_manager import JobManager; jm = JobManager(); print(jm.get_current_job())" - -# Access container shell -docker-compose exec ai-trader bash -``` - ---- - -## Part 3: Windmill Integration - -### 3.1 Windmill Overview - -Windmill (windmill.dev) is a workflow automation platform that can: -- Schedule cron jobs -- Execute TypeScript/Python scripts -- Store state between runs -- Build UI dashboards - -**Integration approach:** -1. Windmill cron job triggers simulation daily -2. Windmill polls for job completion -3. Windmill retrieves results and stores in internal database -4. Windmill dashboard displays performance metrics - -### 3.2 Flow 1: Daily Simulation Trigger - -**File:** `windmill/trigger_simulation.ts` - -```typescript -import { Resource } from "https://deno.land/x/windmill@v1.0.0/mod.ts"; - -export async function main( - ai_trader_api: Resource<"ai_trader_api"> -) { - const apiUrl = ai_trader_api.base_url; // e.g., "http://ai-trader:8080" - - // Trigger simulation - const response = await fetch(`${apiUrl}/simulate/trigger`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - config_path: "configs/default_config.json" - }), - }); - - if (!response.ok) { - throw new Error(`API error: ${response.status} ${response.statusText}`); - } - - const data = await response.json(); - - // Handle different response types - if (data.status === "current") { - console.log("Simulation already up-to-date"); - return { - action: "skipped", - message: data.message, - last_date: data.last_simulation_date - }; - } - - // Store job_id in Windmill state for poller to pick up - await Deno.writeTextFile( - `/tmp/current_job_id.txt`, - data.job_id - ); - - console.log(`Simulation triggered: ${data.job_id}`); - console.log(`Date range: ${data.date_range.join(", ")}`); - console.log(`Models: ${data.models.join(", ")}`); - - return { - action: "triggered", - job_id: data.job_id, - date_range: data.date_range, - models: data.models, - status: data.status - }; -} -``` - -**Windmill Resource Configuration:** -```json -{ - "resource_type": "ai_trader_api", - "base_url": "http://ai-trader:8080" -} -``` - -**Schedule:** Every day at 6:00 AM - ---- - -### 3.3 Flow 2: Job Status Poller - -**File:** `windmill/poll_simulation_status.ts` - -```typescript -import { Resource } from "https://deno.land/x/windmill@v1.0.0/mod.ts"; - -export async function main( - ai_trader_api: Resource<"ai_trader_api">, - job_id?: string -) { - const apiUrl = ai_trader_api.base_url; - - // Get job_id from parameter or from current job file - let jobId = job_id; - if (!jobId) { - try { - jobId = await Deno.readTextFile("/tmp/current_job_id.txt"); - } catch { - // No current job - return { - status: "no_job", - message: "No active simulation job" - }; - } - } - - // Poll status - const response = await fetch(`${apiUrl}/simulate/status/${jobId}`); - - if (!response.ok) { - if (response.status === 404) { - return { - status: "not_found", - message: "Job not found", - job_id: jobId - }; - } - throw new Error(`API error: ${response.status}`); - } - - const data = await response.json(); - - console.log(`Job ${jobId}: ${data.status}`); - console.log(`Progress: ${data.progress.completed}/${data.progress.total_model_days} model-days`); - - // If job is complete, retrieve results - if (data.status === "completed" || data.status === "partial") { - console.log("Job finished, retrieving results..."); - - const results = []; - for (const date of data.date_range) { - const resultsResponse = await fetch( - `${apiUrl}/results?date=${date}&detail=minimal` - ); - - if (resultsResponse.ok) { - const dateResults = await resultsResponse.json(); - results.push(dateResults); - } - } - - // Clean up job_id file - try { - await Deno.remove("/tmp/current_job_id.txt"); - } catch { - // Ignore - } - - return { - status: data.status, - job_id: jobId, - completed_at: data.completed_at, - duration_seconds: data.total_duration_seconds, - results: results - }; - } - - // Job still running - return { - status: data.status, - job_id: jobId, - progress: data.progress, - started_at: data.created_at - }; -} -``` - -**Schedule:** Every 5 minutes (will skip if no active job) - ---- - -### 3.4 Flow 3: Results Retrieval and Storage - -**File:** `windmill/store_simulation_results.py` - -```python -import wmill -from datetime import datetime - -def main( - job_results: dict, - database: str = "simulation_results" -): - """ - Store simulation results in Windmill's internal database. - - Args: - job_results: Output from poll_simulation_status flow - database: Database name for storage - """ - if job_results.get("status") not in ("completed", "partial"): - return {"message": "Job not complete, skipping storage"} - - # Extract results - job_id = job_results["job_id"] - results = job_results.get("results", []) - - stored_count = 0 - - for date_result in results: - date = date_result["date"] - - for model_result in date_result["results"]: - model = model_result["model"] - positions = model_result["positions"] - pnl = model_result["daily_pnl"] - - # Store in Windmill database - record = { - "job_id": job_id, - "date": date, - "model": model, - "cash": positions.get("CASH", 0), - "portfolio_value": pnl["portfolio_value"], - "daily_profit": pnl["profit"], - "daily_return_pct": pnl["return_pct"], - "stored_at": datetime.utcnow().isoformat() - } - - # Use Windmill's internal storage - wmill.set_variable( - path=f"{database}/{model}/{date}", - value=record - ) - - stored_count += 1 - - return { - "stored_count": stored_count, - "job_id": job_id, - "message": f"Stored {stored_count} model-day results" - } -``` - ---- - -### 3.5 Windmill Dashboard Example - -**File:** `windmill/dashboard.json` (Windmill App Builder) - -```json -{ - "grid": [ - { - "type": "table", - "id": "performance_table", - "configuration": { - "title": "Model Performance Summary", - "data_source": { - "type": "script", - "path": "f/simulation_results/get_latest_performance" - }, - "columns": [ - {"field": "model", "header": "Model"}, - {"field": "latest_date", "header": "Latest Date"}, - {"field": "portfolio_value", "header": "Portfolio Value"}, - {"field": "total_return_pct", "header": "Total Return %"}, - {"field": "daily_return_pct", "header": "Daily Return %"} - ] - } - }, - { - "type": "chart", - "id": "portfolio_chart", - "configuration": { - "title": "Portfolio Value Over Time", - "chart_type": "line", - "data_source": { - "type": "script", - "path": "f/simulation_results/get_timeseries" - }, - "x_axis": "date", - "y_axis": "portfolio_value", - "series": "model" - } - } - ] -} -``` - -**Supporting Script:** `windmill/get_latest_performance.py` - -```python -import wmill - -def main(database: str = "simulation_results"): - """Get latest performance for each model""" - - # Query Windmill variables - all_vars = wmill.list_variables(path_prefix=f"{database}/") - - # Group by model - models = {} - for var in all_vars: - parts = var["path"].split("/") - if len(parts) >= 3: - model = parts[1] - date = parts[2] - - value = wmill.get_variable(var["path"]) - - if model not in models: - models[model] = [] - models[model].append(value) - - # Compute summary for each model - summary = [] - for model, records in models.items(): - # Sort by date - records.sort(key=lambda x: x["date"], reverse=True) - latest = records[0] - - # Calculate total return - initial_value = 10000 # Initial cash - total_return_pct = ((latest["portfolio_value"] - initial_value) / initial_value) * 100 - - summary.append({ - "model": model, - "latest_date": latest["date"], - "portfolio_value": latest["portfolio_value"], - "total_return_pct": round(total_return_pct, 2), - "daily_return_pct": latest["daily_return_pct"] - }) - - return summary -``` - ---- - -### 3.6 Windmill Workflow Orchestration - -**Main Workflow:** `windmill/daily_simulation_workflow.yaml` - -```yaml -name: Daily AI Trader Simulation -description: Trigger simulation, poll status, and store results - -triggers: - - type: cron - schedule: "0 6 * * *" # Every day at 6 AM - -steps: - - id: trigger - name: Trigger Simulation - script: f/ai_trader/trigger_simulation - outputs: - - job_id - - action - - - id: wait - name: Wait for Job Start - type: sleep - duration: 10s - - - id: poll_loop - name: Poll Until Complete - type: loop - max_iterations: 60 # Poll for up to 5 hours (60 ร— 5min) - interval: 5m - script: f/ai_trader/poll_simulation_status - inputs: - job_id: ${{ steps.trigger.outputs.job_id }} - break_condition: | - ${{ steps.poll_loop.outputs.status in ['completed', 'partial', 'failed'] }} - - - id: store_results - name: Store Results in Database - script: f/ai_trader/store_simulation_results - inputs: - job_results: ${{ steps.poll_loop.outputs }} - condition: | - ${{ steps.poll_loop.outputs.status in ['completed', 'partial'] }} - - - id: notify - name: Send Notification - type: email - to: admin@example.com - subject: "AI Trader Simulation Complete" - body: | - Simulation completed for ${{ steps.poll_loop.outputs.job_id }} - Status: ${{ steps.poll_loop.outputs.status }} - Duration: ${{ steps.poll_loop.outputs.duration_seconds }}s -``` - ---- - -### 3.7 Testing Windmill Integration Locally - -**1. Start AI-Trader API:** -```bash -docker-compose up -d -``` - -**2. Test trigger endpoint:** -```bash -curl -X POST http://localhost:8080/simulate/trigger \ - -H "Content-Type: application/json" \ - -d '{"config_path": "configs/default_config.json"}' -``` - -**3. Test status polling:** -```bash -JOB_ID="" -curl http://localhost:8080/simulate/status/$JOB_ID -``` - -**4. Test results retrieval:** -```bash -curl "http://localhost:8080/results?date=2025-01-16&model=gpt-5&detail=minimal" -``` - -**5. Deploy to Windmill:** -```bash -# Install Windmill CLI -npm install -g windmill-cli - -# Login to your Windmill instance -wmill login https://your-windmill-instance.com - -# Deploy scripts -wmill script push windmill/trigger_simulation.ts -wmill script push windmill/poll_simulation_status.ts -wmill script push windmill/store_simulation_results.py - -# Deploy workflow -wmill flow push windmill/daily_simulation_workflow.yaml -``` - ---- - -## Part 4: Complete File Structure - -After implementation, the project structure will be: - -``` -AI-Trader/ -โ”œโ”€โ”€ api/ -โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”œโ”€โ”€ main.py # FastAPI application -โ”‚ โ”œโ”€โ”€ models.py # Pydantic request/response models -โ”‚ โ”œโ”€โ”€ job_manager.py # Job lifecycle management -โ”‚ โ”œโ”€โ”€ database.py # SQLite utilities -โ”‚ โ”œโ”€โ”€ worker.py # Background simulation worker -โ”‚ โ”œโ”€โ”€ executor.py # Single model-day execution -โ”‚ โ””โ”€โ”€ runtime_manager.py # Runtime config isolation -โ”‚ -โ”œโ”€โ”€ docs/ -โ”‚ โ”œโ”€โ”€ api-specification.md -โ”‚ โ”œโ”€โ”€ job-manager-specification.md -โ”‚ โ”œโ”€โ”€ worker-specification.md -โ”‚ โ””โ”€โ”€ implementation-specifications.md -โ”‚ -โ”œโ”€โ”€ windmill/ -โ”‚ โ”œโ”€โ”€ trigger_simulation.ts -โ”‚ โ”œโ”€โ”€ poll_simulation_status.ts -โ”‚ โ”œโ”€โ”€ store_simulation_results.py -โ”‚ โ”œโ”€โ”€ get_latest_performance.py -โ”‚ โ”œโ”€โ”€ daily_simulation_workflow.yaml -โ”‚ โ””โ”€โ”€ dashboard.json -โ”‚ -โ”œโ”€โ”€ agent/ -โ”‚ โ””โ”€โ”€ base_agent/ -โ”‚ โ””โ”€โ”€ base_agent.py # NO CHANGES NEEDED -โ”‚ -โ”œโ”€โ”€ agent_tools/ -โ”‚ โ””โ”€โ”€ ... (existing MCP tools) -โ”‚ -โ”œโ”€โ”€ data/ -โ”‚ โ”œโ”€โ”€ jobs.db # SQLite database (created automatically) -โ”‚ โ”œโ”€โ”€ runtime_env*.json # Runtime configs (temporary) -โ”‚ โ”œโ”€โ”€ agent_data/ # Existing position/log data -โ”‚ โ””โ”€โ”€ merged.jsonl # Existing price data -โ”‚ -โ”œโ”€โ”€ Dockerfile # Updated for API mode -โ”œโ”€โ”€ docker-compose.yml # Updated service definition -โ”œโ”€โ”€ docker-entrypoint-api.sh # New API entrypoint -โ”œโ”€โ”€ requirements-api.txt # FastAPI dependencies -โ”œโ”€โ”€ .env # Environment configuration -โ””โ”€โ”€ main.py # Existing (used by worker) -``` - ---- - -## Part 5: Implementation Checklist - -### Phase 1: API Foundation (Days 1-2) -- [ ] Create `api/` directory structure -- [ ] Implement `api/models.py` with Pydantic models -- [ ] Implement `api/database.py` with SQLite utilities -- [ ] Implement `api/job_manager.py` with job CRUD operations -- [ ] Write unit tests for job_manager -- [ ] Test database operations manually - -### Phase 2: Worker & Executor (Days 3-4) -- [ ] Implement `api/runtime_manager.py` -- [ ] Implement `api/executor.py` for single model-day execution -- [ ] Implement `api/worker.py` for job orchestration -- [ ] Test worker with mock agent -- [ ] Test runtime config isolation - -### Phase 3: FastAPI Endpoints (Days 5-6) -- [ ] Implement `api/main.py` with all endpoints -- [ ] Implement `/simulate/trigger` with background tasks -- [ ] Implement `/simulate/status/{job_id}` -- [ ] Implement `/simulate/current` -- [ ] Implement `/results` with detail levels -- [ ] Implement `/health` with MCP checks -- [ ] Test all endpoints with Postman/curl - -### Phase 4: Docker Integration (Day 7) -- [ ] Update `Dockerfile` -- [ ] Create `docker-entrypoint-api.sh` -- [ ] Create `requirements-api.txt` -- [ ] Update `docker-compose.yml` -- [ ] Test Docker build -- [ ] Test container startup and health checks -- [ ] Test end-to-end simulation via API in Docker - -### Phase 5: Windmill Integration (Days 8-9) -- [ ] Create Windmill scripts (trigger, poll, store) -- [ ] Test scripts locally against Docker API -- [ ] Deploy scripts to Windmill instance -- [ ] Create Windmill workflow -- [ ] Test workflow end-to-end -- [ ] Create Windmill dashboard -- [ ] Document Windmill setup process - -### Phase 6: Testing & Documentation (Day 10) -- [ ] Integration tests for complete workflow -- [ ] Load testing (multiple concurrent requests) -- [ ] Error scenario testing (MCP down, API timeout) -- [ ] Update README.md with API usage -- [ ] Create API documentation (Swagger/OpenAPI) -- [ ] Create deployment guide -- [ ] Create troubleshooting guide - ---- - -## Summary - -This comprehensive specification covers: - -1. **BaseAgent Refactoring:** Minimal changes needed (existing code compatible) -2. **Docker Configuration:** API service mode with health checks and proper entrypoint -3. **Windmill Integration:** Complete workflow automation with TypeScript/Python scripts -4. **File Structure:** Clear organization of new API components -5. **Implementation Checklist:** Step-by-step plan for 10-day implementation - -**Total estimated implementation time:** 10 working days for MVP - -**Next Step:** Review all specifications (api-specification.md, job-manager-specification.md, worker-specification.md, and this document) and approve before beginning implementation. diff --git a/docs/job-manager-specification.md b/docs/job-manager-specification.md deleted file mode 100644 index f1f846d..0000000 --- a/docs/job-manager-specification.md +++ /dev/null @@ -1,963 +0,0 @@ -# Job Manager & Database Specification - -## 1. Overview - -The Job Manager is responsible for: -1. **Job lifecycle management** - Creating, tracking, updating job status -2. **Database operations** - SQLite CRUD operations for jobs and job_details -3. **Concurrency control** - Ensuring only one simulation runs at a time -4. **State persistence** - Maintaining job state across API restarts - ---- - -## 2. Database Schema - -### 2.1 SQLite Database Location - -``` -data/jobs.db -``` - -**Rationale:** Co-located with simulation data for easy volume mounting - -### 2.2 Table: jobs - -**Purpose:** Track high-level job metadata and status - -```sql -CREATE TABLE IF NOT EXISTS jobs ( - job_id TEXT PRIMARY KEY, - config_path TEXT NOT NULL, - status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'partial', 'failed')), - date_range TEXT NOT NULL, -- JSON array: ["2025-01-16", "2025-01-17"] - models TEXT NOT NULL, -- JSON array: ["claude-3.7-sonnet", "gpt-5"] - created_at TEXT NOT NULL, -- ISO 8601: "2025-01-20T14:30:00Z" - started_at TEXT, -- When first model-day started - completed_at TEXT, -- When last model-day finished - total_duration_seconds REAL, - error TEXT -- Top-level error message if job failed -); - --- Indexes for performance -CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status); -CREATE INDEX IF NOT EXISTS idx_jobs_created_at ON jobs(created_at DESC); -``` - -**Field Details:** -- `job_id`: UUID v4 (e.g., `550e8400-e29b-41d4-a716-446655440000`) -- `status`: Current job state - - `pending`: Job created, not started yet - - `running`: At least one model-day is executing - - `completed`: All model-days succeeded - - `partial`: Some model-days succeeded, some failed - - `failed`: All model-days failed (rare edge case) -- `date_range`: JSON string for easy querying -- `models`: JSON string of enabled model signatures - -### 2.3 Table: job_details - -**Purpose:** Track individual model-day execution status - -```sql -CREATE TABLE IF NOT EXISTS job_details ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - job_id TEXT NOT NULL, - date TEXT NOT NULL, -- "2025-01-16" - model TEXT NOT NULL, -- "gpt-5" - status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed')), - started_at TEXT, - completed_at TEXT, - duration_seconds REAL, - error TEXT, -- Error message if this model-day failed - FOREIGN KEY (job_id) REFERENCES jobs(job_id) ON DELETE CASCADE -); - --- Indexes -CREATE INDEX IF NOT EXISTS idx_job_details_job_id ON job_details(job_id); -CREATE INDEX IF NOT EXISTS idx_job_details_status ON job_details(status); -CREATE UNIQUE INDEX IF NOT EXISTS idx_job_details_unique ON job_details(job_id, date, model); -``` - -**Field Details:** -- Each row represents one model-day (e.g., `gpt-5` on `2025-01-16`) -- `UNIQUE INDEX` prevents duplicate execution entries -- `ON DELETE CASCADE` ensures orphaned records are cleaned up - -### 2.4 Example Data - -**jobs table:** -``` -job_id | config_path | status | date_range | models | created_at | started_at | completed_at | total_duration_seconds ---------------------------------------|--------------------------|-----------|-----------------------------------|---------------------------------|----------------------|----------------------|----------------------|---------------------- -550e8400-e29b-41d4-a716-446655440000 | configs/default_config.json | completed | ["2025-01-16","2025-01-17"] | ["gpt-5","claude-3.7-sonnet"] | 2025-01-20T14:25:00Z | 2025-01-20T14:25:10Z | 2025-01-20T14:29:45Z | 275.3 -``` - -**job_details table:** -``` -id | job_id | date | model | status | started_at | completed_at | duration_seconds | error ----|--------------------------------------|------------|--------------------|-----------|----------------------|----------------------|------------------|------ -1 | 550e8400-e29b-41d4-a716-446655440000 | 2025-01-16 | gpt-5 | completed | 2025-01-20T14:25:10Z | 2025-01-20T14:25:48Z | 38.2 | NULL -2 | 550e8400-e29b-41d4-a716-446655440000 | 2025-01-16 | claude-3.7-sonnet | completed | 2025-01-20T14:25:10Z | 2025-01-20T14:25:55Z | 45.1 | NULL -3 | 550e8400-e29b-41d4-a716-446655440000 | 2025-01-17 | gpt-5 | completed | 2025-01-20T14:25:56Z | 2025-01-20T14:26:36Z | 40.0 | NULL -4 | 550e8400-e29b-41d4-a716-446655440000 | 2025-01-17 | claude-3.7-sonnet | completed | 2025-01-20T14:25:56Z | 2025-01-20T14:26:42Z | 46.5 | NULL -``` - ---- - -## 3. Job Manager Class - -### 3.1 File Structure - -``` -api/ -โ”œโ”€โ”€ job_manager.py # Core JobManager class -โ”œโ”€โ”€ database.py # SQLite connection and utilities -โ””โ”€โ”€ models.py # Pydantic models -``` - -### 3.2 JobManager Interface - -```python -# api/job_manager.py - -from datetime import datetime -from typing import Optional, List, Dict, Tuple -import uuid -import json -from api.database import get_db_connection - -class JobManager: - """Manages simulation job lifecycle and database operations""" - - def __init__(self, db_path: str = "data/jobs.db"): - self.db_path = db_path - self._initialize_database() - - def _initialize_database(self) -> None: - """Create tables if they don't exist""" - conn = get_db_connection(self.db_path) - # Execute CREATE TABLE statements from section 2.2 and 2.3 - conn.close() - - # ========== Job Creation ========== - - def create_job( - self, - config_path: str, - date_range: List[str], - models: List[str] - ) -> str: - """ - Create a new simulation job. - - Args: - config_path: Path to config file - date_range: List of trading dates to simulate - models: List of model signatures to run - - Returns: - job_id: UUID of created job - - Raises: - ValueError: If another job is already running - """ - # 1. Check if any jobs are currently running - if not self.can_start_new_job(): - raise ValueError("Another simulation job is already running") - - # 2. Generate job ID - job_id = str(uuid.uuid4()) - - # 3. Create job record - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - cursor.execute(""" - INSERT INTO jobs ( - job_id, config_path, status, date_range, models, created_at - ) VALUES (?, ?, ?, ?, ?, ?) - """, ( - job_id, - config_path, - "pending", - json.dumps(date_range), - json.dumps(models), - datetime.utcnow().isoformat() + "Z" - )) - - # 4. Create job_details records for each model-day - for date in date_range: - for model in models: - cursor.execute(""" - INSERT INTO job_details ( - job_id, date, model, status - ) VALUES (?, ?, ?, ?) - """, (job_id, date, model, "pending")) - - conn.commit() - conn.close() - - return job_id - - # ========== Job Retrieval ========== - - def get_job(self, job_id: str) -> Optional[Dict]: - """ - Get job metadata by ID. - - Returns: - Job dict with keys: job_id, config_path, status, date_range (list), - models (list), created_at, started_at, completed_at, total_duration_seconds - - Returns None if job not found. - """ - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - cursor.execute("SELECT * FROM jobs WHERE job_id = ?", (job_id,)) - row = cursor.fetchone() - conn.close() - - if row is None: - return None - - return { - "job_id": row[0], - "config_path": row[1], - "status": row[2], - "date_range": json.loads(row[3]), - "models": json.loads(row[4]), - "created_at": row[5], - "started_at": row[6], - "completed_at": row[7], - "total_duration_seconds": row[8], - "error": row[9] - } - - def get_current_job(self) -> Optional[Dict]: - """Get most recent job (for /simulate/current endpoint)""" - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - cursor.execute(""" - SELECT * FROM jobs - ORDER BY created_at DESC - LIMIT 1 - """) - row = cursor.fetchone() - conn.close() - - if row is None: - return None - - return self._row_to_job_dict(row) - - def get_running_jobs(self) -> List[Dict]: - """Get all running or pending jobs""" - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - cursor.execute(""" - SELECT * FROM jobs - WHERE status IN ('pending', 'running') - ORDER BY created_at DESC - """) - rows = cursor.fetchall() - conn.close() - - return [self._row_to_job_dict(row) for row in rows] - - # ========== Job Status Updates ========== - - def update_job_status( - self, - job_id: str, - status: str, - error: Optional[str] = None - ) -> None: - """Update job status (pending โ†’ running โ†’ completed/partial/failed)""" - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - updates = {"status": status} - - if status == "running" and self.get_job(job_id)["status"] == "pending": - updates["started_at"] = datetime.utcnow().isoformat() + "Z" - - if status in ("completed", "partial", "failed"): - updates["completed_at"] = datetime.utcnow().isoformat() + "Z" - # Calculate total duration - job = self.get_job(job_id) - if job["started_at"]: - started = datetime.fromisoformat(job["started_at"].replace("Z", "")) - completed = datetime.utcnow() - updates["total_duration_seconds"] = (completed - started).total_seconds() - - if error: - updates["error"] = error - - # Build dynamic UPDATE query - set_clause = ", ".join([f"{k} = ?" for k in updates.keys()]) - values = list(updates.values()) + [job_id] - - cursor.execute(f""" - UPDATE jobs - SET {set_clause} - WHERE job_id = ? - """, values) - - conn.commit() - conn.close() - - def update_job_detail_status( - self, - job_id: str, - date: str, - model: str, - status: str, - error: Optional[str] = None - ) -> None: - """Update individual model-day status""" - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - updates = {"status": status} - - # Get current detail status to determine if this is a status transition - cursor.execute(""" - SELECT status, started_at FROM job_details - WHERE job_id = ? AND date = ? AND model = ? - """, (job_id, date, model)) - row = cursor.fetchone() - - if row: - current_status = row[0] - - if status == "running" and current_status == "pending": - updates["started_at"] = datetime.utcnow().isoformat() + "Z" - - if status in ("completed", "failed"): - updates["completed_at"] = datetime.utcnow().isoformat() + "Z" - # Calculate duration if started_at exists - if row[1]: # started_at - started = datetime.fromisoformat(row[1].replace("Z", "")) - completed = datetime.utcnow() - updates["duration_seconds"] = (completed - started).total_seconds() - - if error: - updates["error"] = error - - # Build UPDATE query - set_clause = ", ".join([f"{k} = ?" for k in updates.keys()]) - values = list(updates.values()) + [job_id, date, model] - - cursor.execute(f""" - UPDATE job_details - SET {set_clause} - WHERE job_id = ? AND date = ? AND model = ? - """, values) - - conn.commit() - conn.close() - - # After updating detail, check if overall job status needs update - self._update_job_status_from_details(job_id) - - def _update_job_status_from_details(self, job_id: str) -> None: - """ - Recalculate job status based on job_details statuses. - - Logic: - - If any detail is 'running' โ†’ job is 'running' - - If all details are 'completed' โ†’ job is 'completed' - - If some details are 'completed' and some 'failed' โ†’ job is 'partial' - - If all details are 'failed' โ†’ job is 'failed' - - If all details are 'pending' โ†’ job is 'pending' - """ - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - cursor.execute(""" - SELECT status, COUNT(*) - FROM job_details - WHERE job_id = ? - GROUP BY status - """, (job_id,)) - - status_counts = {row[0]: row[1] for row in cursor.fetchall()} - conn.close() - - # Determine overall job status - if status_counts.get("running", 0) > 0: - new_status = "running" - elif status_counts.get("pending", 0) > 0: - # Some details still pending, job is either pending or running - current_job = self.get_job(job_id) - new_status = current_job["status"] # Keep current status - elif status_counts.get("failed", 0) > 0 and status_counts.get("completed", 0) > 0: - new_status = "partial" - elif status_counts.get("failed", 0) > 0: - new_status = "failed" - else: - new_status = "completed" - - self.update_job_status(job_id, new_status) - - # ========== Job Progress ========== - - def get_job_progress(self, job_id: str) -> Dict: - """ - Get detailed progress for a job. - - Returns: - { - "total_model_days": int, - "completed": int, - "failed": int, - "current": {"date": str, "model": str} | None, - "details": [ - {"date": str, "model": str, "status": str, "duration_seconds": float | None, "error": str | None}, - ... - ] - } - """ - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - # Get all details for this job - cursor.execute(""" - SELECT date, model, status, started_at, completed_at, duration_seconds, error - FROM job_details - WHERE job_id = ? - ORDER BY date ASC, model ASC - """, (job_id,)) - - rows = cursor.fetchall() - conn.close() - - if not rows: - return { - "total_model_days": 0, - "completed": 0, - "failed": 0, - "current": None, - "details": [] - } - - total = len(rows) - completed = sum(1 for row in rows if row[2] == "completed") - failed = sum(1 for row in rows if row[2] == "failed") - - # Find currently running model-day - current = None - for row in rows: - if row[2] == "running": - current = {"date": row[0], "model": row[1]} - break - - # Build details list - details = [] - for row in rows: - details.append({ - "date": row[0], - "model": row[1], - "status": row[2], - "started_at": row[3], - "completed_at": row[4], - "duration_seconds": row[5], - "error": row[6] - }) - - return { - "total_model_days": total, - "completed": completed, - "failed": failed, - "current": current, - "details": details - } - - # ========== Concurrency Control ========== - - def can_start_new_job(self) -> bool: - """Check if a new job can be started (max 1 concurrent job)""" - running_jobs = self.get_running_jobs() - return len(running_jobs) == 0 - - def find_job_by_date_range(self, date_range: List[str]) -> Optional[Dict]: - """Find job with exact matching date range (for idempotency check)""" - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - # Query recent jobs (last 24 hours) - cursor.execute(""" - SELECT * FROM jobs - WHERE created_at > datetime('now', '-1 day') - ORDER BY created_at DESC - """) - - rows = cursor.fetchall() - conn.close() - - # Check each job's date_range - target_range = set(date_range) - for row in rows: - job_range = set(json.loads(row[3])) # date_range column - if job_range == target_range: - return self._row_to_job_dict(row) - - return None - - # ========== Utility Methods ========== - - def _row_to_job_dict(self, row: tuple) -> Dict: - """Convert DB row to job dictionary""" - return { - "job_id": row[0], - "config_path": row[1], - "status": row[2], - "date_range": json.loads(row[3]), - "models": json.loads(row[4]), - "created_at": row[5], - "started_at": row[6], - "completed_at": row[7], - "total_duration_seconds": row[8], - "error": row[9] - } - - def cleanup_old_jobs(self, days: int = 30) -> int: - """ - Delete jobs older than specified days (cleanup maintenance). - - Returns: - Number of jobs deleted - """ - conn = get_db_connection(self.db_path) - cursor = conn.cursor() - - cursor.execute(""" - DELETE FROM jobs - WHERE created_at < datetime('now', '-' || ? || ' days') - """, (days,)) - - deleted_count = cursor.rowcount - conn.commit() - conn.close() - - return deleted_count -``` - ---- - -## 4. Database Utility Module - -```python -# api/database.py - -import sqlite3 -from typing import Optional -import os - -def get_db_connection(db_path: str = "data/jobs.db") -> sqlite3.Connection: - """ - Get SQLite database connection. - - Ensures: - - Database directory exists - - Foreign keys are enabled - - Row factory returns dict-like objects - """ - # Ensure data directory exists - os.makedirs(os.path.dirname(db_path), exist_ok=True) - - conn = sqlite3.connect(db_path, check_same_thread=False) - conn.execute("PRAGMA foreign_keys = ON") # Enable FK constraints - conn.row_factory = sqlite3.Row # Return rows as dict-like objects - - return conn - -def initialize_database(db_path: str = "data/jobs.db") -> None: - """Create database tables if they don't exist""" - conn = get_db_connection(db_path) - cursor = conn.cursor() - - # Create jobs table - cursor.execute(""" - CREATE TABLE IF NOT EXISTS jobs ( - job_id TEXT PRIMARY KEY, - config_path TEXT NOT NULL, - status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'partial', 'failed')), - date_range TEXT NOT NULL, - models TEXT NOT NULL, - created_at TEXT NOT NULL, - started_at TEXT, - completed_at TEXT, - total_duration_seconds REAL, - error TEXT - ) - """) - - # Create indexes - cursor.execute(""" - CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status) - """) - cursor.execute(""" - CREATE INDEX IF NOT EXISTS idx_jobs_created_at ON jobs(created_at DESC) - """) - - # Create job_details table - cursor.execute(""" - CREATE TABLE IF NOT EXISTS job_details ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - job_id TEXT NOT NULL, - date TEXT NOT NULL, - model TEXT NOT NULL, - status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed')), - started_at TEXT, - completed_at TEXT, - duration_seconds REAL, - error TEXT, - FOREIGN KEY (job_id) REFERENCES jobs(job_id) ON DELETE CASCADE - ) - """) - - # Create indexes - cursor.execute(""" - CREATE INDEX IF NOT EXISTS idx_job_details_job_id ON job_details(job_id) - """) - cursor.execute(""" - CREATE INDEX IF NOT EXISTS idx_job_details_status ON job_details(status) - """) - cursor.execute(""" - CREATE UNIQUE INDEX IF NOT EXISTS idx_job_details_unique - ON job_details(job_id, date, model) - """) - - conn.commit() - conn.close() -``` - ---- - -## 5. State Transitions - -### 5.1 Job Status State Machine - -``` -pending โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> running โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> completed - โ”‚ โ”‚ - โ”‚ โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> partial - โ”‚ โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> failed -``` - -**Transition Logic:** -- `pending โ†’ running`: When first model-day starts executing -- `running โ†’ completed`: When all model-days complete successfully -- `running โ†’ partial`: When some model-days succeed, some fail -- `running โ†’ failed`: When all model-days fail (rare) - -### 5.2 Job Detail Status State Machine - -``` -pending โ”€โ”€โ”€โ”€โ”€โ”€> running โ”€โ”€โ”€โ”€โ”€โ”€> completed - โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> failed -``` - -**Transition Logic:** -- `pending โ†’ running`: When worker starts executing that model-day -- `running โ†’ completed`: When `agent.run_trading_session()` succeeds -- `running โ†’ failed`: When `agent.run_trading_session()` raises exception after retries - ---- - -## 6. Concurrency Scenarios - -### 6.1 Scenario: Duplicate Trigger Requests - -**Timeline:** -1. Request A: POST /simulate/trigger โ†’ Job created with date_range=[2025-01-16, 2025-01-17] -2. Request B (5 seconds later): POST /simulate/trigger โ†’ Same date range - -**Expected Behavior:** -- Request A: Returns `{"job_id": "abc123", "status": "accepted"}` -- Request B: `find_job_by_date_range()` finds Job abc123 -- Request B: Returns `{"job_id": "abc123", "status": "running", ...}` (same job) - -**Code:** -```python -# In /simulate/trigger endpoint -existing_job = job_manager.find_job_by_date_range(date_range) -if existing_job: - # Return existing job instead of creating duplicate - return existing_job -``` - -### 6.2 Scenario: Concurrent Jobs with Different Dates - -**Timeline:** -1. Job A running: date_range=[2025-01-01 to 2025-01-10] (started 5 min ago) -2. Request: POST /simulate/trigger with date_range=[2025-01-11 to 2025-01-15] - -**Expected Behavior:** -- `can_start_new_job()` returns False (Job A is still running) -- Request returns 409 Conflict with details of Job A - -### 6.3 Scenario: Job Cleanup on API Restart - -**Problem:** API crashes while job is running. On restart, job stuck in "running" state. - -**Solution:** On API startup, detect stale jobs and mark as failed: -```python -# In api/main.py startup event -@app.on_event("startup") -async def startup_event(): - job_manager = JobManager() - - # Find jobs stuck in 'running' or 'pending' state - stale_jobs = job_manager.get_running_jobs() - - for job in stale_jobs: - # Mark as failed with explanation - job_manager.update_job_status( - job["job_id"], - "failed", - error="API restarted while job was running" - ) -``` - ---- - -## 7. Testing Strategy - -### 7.1 Unit Tests - -```python -# tests/test_job_manager.py - -import pytest -from api.job_manager import JobManager -import tempfile -import os - -@pytest.fixture -def job_manager(): - # Use temporary database for tests - temp_db = tempfile.NamedTemporaryFile(delete=False, suffix=".db") - temp_db.close() - - jm = JobManager(db_path=temp_db.name) - yield jm - - # Cleanup - os.unlink(temp_db.name) - -def test_create_job(job_manager): - job_id = job_manager.create_job( - config_path="configs/test.json", - date_range=["2025-01-16", "2025-01-17"], - models=["gpt-5", "claude-3.7-sonnet"] - ) - - assert job_id is not None - job = job_manager.get_job(job_id) - assert job["status"] == "pending" - assert job["date_range"] == ["2025-01-16", "2025-01-17"] - - # Check job_details created - progress = job_manager.get_job_progress(job_id) - assert progress["total_model_days"] == 4 # 2 dates ร— 2 models - -def test_concurrent_job_blocked(job_manager): - # Create first job - job1_id = job_manager.create_job("configs/test.json", ["2025-01-16"], ["gpt-5"]) - - # Try to create second job while first is pending - with pytest.raises(ValueError, match="Another simulation job is already running"): - job_manager.create_job("configs/test.json", ["2025-01-17"], ["gpt-5"]) - - # Mark first job as completed - job_manager.update_job_status(job1_id, "completed") - - # Now second job should be allowed - job2_id = job_manager.create_job("configs/test.json", ["2025-01-17"], ["gpt-5"]) - assert job2_id is not None - -def test_job_status_transitions(job_manager): - job_id = job_manager.create_job("configs/test.json", ["2025-01-16"], ["gpt-5"]) - - # Update job detail to running - job_manager.update_job_detail_status(job_id, "2025-01-16", "gpt-5", "running") - - # Job should now be 'running' - job = job_manager.get_job(job_id) - assert job["status"] == "running" - assert job["started_at"] is not None - - # Complete the detail - job_manager.update_job_detail_status(job_id, "2025-01-16", "gpt-5", "completed") - - # Job should now be 'completed' - job = job_manager.get_job(job_id) - assert job["status"] == "completed" - assert job["completed_at"] is not None - -def test_partial_job_status(job_manager): - job_id = job_manager.create_job( - "configs/test.json", - ["2025-01-16"], - ["gpt-5", "claude-3.7-sonnet"] - ) - - # One model succeeds - job_manager.update_job_detail_status(job_id, "2025-01-16", "gpt-5", "running") - job_manager.update_job_detail_status(job_id, "2025-01-16", "gpt-5", "completed") - - # One model fails - job_manager.update_job_detail_status(job_id, "2025-01-16", "claude-3.7-sonnet", "running") - job_manager.update_job_detail_status( - job_id, "2025-01-16", "claude-3.7-sonnet", "failed", - error="API timeout" - ) - - # Job should be 'partial' - job = job_manager.get_job(job_id) - assert job["status"] == "partial" - - progress = job_manager.get_job_progress(job_id) - assert progress["completed"] == 1 - assert progress["failed"] == 1 -``` - ---- - -## 8. Performance Considerations - -### 8.1 Database Indexing - -- `idx_jobs_status`: Fast filtering for running jobs -- `idx_jobs_created_at DESC`: Fast retrieval of most recent job -- `idx_job_details_unique`: Prevent duplicate model-day entries - -### 8.2 Connection Pooling - -For MVP, using `sqlite3.connect()` per operation is acceptable (low concurrency). - -For higher concurrency (future), consider: -- SQLAlchemy ORM with connection pooling -- PostgreSQL for production deployments - -### 8.3 Query Optimization - -**Avoid N+1 queries:** -```python -# BAD: Separate query for each job's progress -for job in jobs: - progress = job_manager.get_job_progress(job["job_id"]) - -# GOOD: Join jobs and job_details in single query -SELECT - jobs.*, - COUNT(job_details.id) as total, - SUM(CASE WHEN job_details.status = 'completed' THEN 1 ELSE 0 END) as completed -FROM jobs -LEFT JOIN job_details ON jobs.job_id = job_details.job_id -GROUP BY jobs.job_id -``` - ---- - -## 9. Error Handling - -### 9.1 Database Errors - -**Scenario:** SQLite database is locked or corrupted - -**Handling:** -```python -try: - job_id = job_manager.create_job(...) -except sqlite3.OperationalError as e: - # Database locked - retry with exponential backoff - logger.error(f"Database error: {e}") - raise HTTPException(status_code=503, detail="Database temporarily unavailable") -except sqlite3.IntegrityError as e: - # Constraint violation (e.g., duplicate job_id) - logger.error(f"Integrity error: {e}") - raise HTTPException(status_code=400, detail="Invalid job data") -``` - -### 9.2 Foreign Key Violations - -**Scenario:** Attempt to create job_detail for non-existent job - -**Prevention:** -- Always create job record before job_details records -- Use transactions to ensure atomicity - -```python -def create_job(self, ...): - conn = get_db_connection(self.db_path) - try: - cursor = conn.cursor() - - # Insert job - cursor.execute("INSERT INTO jobs ...") - - # Insert job_details - for date in date_range: - for model in models: - cursor.execute("INSERT INTO job_details ...") - - conn.commit() # Atomic commit - except Exception as e: - conn.rollback() # Rollback on any error - raise - finally: - conn.close() -``` - ---- - -## 10. Migration Strategy - -### 10.1 Schema Versioning - -For future schema changes, use migration scripts: - -``` -data/ -โ””โ”€โ”€ migrations/ - โ”œโ”€โ”€ 001_initial_schema.sql - โ”œโ”€โ”€ 002_add_priority_column.sql - โ””โ”€โ”€ ... -``` - -Track applied migrations in database: -```sql -CREATE TABLE IF NOT EXISTS schema_migrations ( - version INTEGER PRIMARY KEY, - applied_at TEXT NOT NULL -); -``` - -### 10.2 Backward Compatibility - -When adding columns: -- Use `ALTER TABLE ADD COLUMN ... DEFAULT ...` for backward compatibility -- Never remove columns (deprecate instead) -- Version API responses to handle schema changes - ---- - -## Summary - -The Job Manager provides: -1. **Robust job tracking** with SQLite persistence -2. **Concurrency control** ensuring single-job execution -3. **Granular progress monitoring** at model-day level -4. **Flexible status handling** (completed/partial/failed) -5. **Idempotency** for duplicate trigger requests - -Next specification: **Background Worker Architecture** diff --git a/docs/plans/2025-10-30-data-cache-reuse-design.md b/docs/plans/2025-10-30-data-cache-reuse-design.md deleted file mode 100644 index 7e4c5f6..0000000 --- a/docs/plans/2025-10-30-data-cache-reuse-design.md +++ /dev/null @@ -1,197 +0,0 @@ -# Data Cache Reuse Design - -**Date:** 2025-10-30 -**Status:** Approved - -## Problem Statement - -Docker containers currently fetch all 103 NASDAQ 100 tickers from Alpha Vantage on every startup, even when price data is volume-mounted and already cached in `./data`. This causes: -- Slow startup times (103 API calls) -- Unnecessary API quota consumption -- Rate limit risks during frequent development iterations - -## Solution Overview - -Implement staleness-based data refresh with configurable age threshold. Container checks all `daily_prices_*.json` files and only refetches if any file is missing or older than `MAX_DATA_AGE_DAYS`. - -## Design Decisions - -### Architecture Choice -**Selected:** Check all `daily_prices_*.json` files individually -**Rationale:** Ensures data integrity by detecting partial/missing files, not just stale merged data - -### Implementation Location -**Selected:** Bash wrapper logic in `entrypoint.sh` -**Rationale:** Keeps data fetching scripts unchanged, adds orchestration at container startup layer - -### Staleness Threshold -**Selected:** Configurable via `MAX_DATA_AGE_DAYS` environment variable (default: 7 days) -**Rationale:** Balances freshness with API usage; flexible for different use cases (development vs production) - -## Technical Design - -### Components - -#### 1. Staleness Check Function -Location: `entrypoint.sh` (after environment validation, before data fetch) - -```bash -should_refresh_data() { - MAX_AGE=${MAX_DATA_AGE_DAYS:-7} - - # Check if at least one price file exists - if ! ls /app/data/daily_prices_*.json >/dev/null 2>&1; then - echo "๐Ÿ“ญ No price data found" - return 0 # Need refresh - fi - - # Find any files older than MAX_AGE days - STALE_COUNT=$(find /app/data -name "daily_prices_*.json" -mtime +$MAX_AGE | wc -l) - TOTAL_COUNT=$(ls /app/data/daily_prices_*.json 2>/dev/null | wc -l) - - if [ $STALE_COUNT -gt 0 ]; then - echo "๐Ÿ“… Found $STALE_COUNT stale files (>$MAX_AGE days old)" - return 0 # Need refresh - fi - - echo "โœ… All $TOTAL_COUNT price files are fresh (<$MAX_AGE days old)" - return 1 # Skip refresh -} -``` - -**Logic:** -- Uses `find -mtime +N` to detect files modified more than N days ago -- Returns shell exit codes: 0 (refresh needed), 1 (skip refresh) -- Logs informative messages for debugging - -#### 2. Conditional Data Fetch -Location: `entrypoint.sh` lines 40-46 (replace existing unconditional fetch) - -```bash -# Step 1: Data preparation (conditional) -echo "๐Ÿ“Š Checking price data freshness..." - -if should_refresh_data; then - echo "๐Ÿ”„ Fetching and merging price data..." - cd /app/data - python /app/scripts/get_daily_price.py - python /app/scripts/merge_jsonl.py - cd /app -else - echo "โญ๏ธ Skipping data fetch (using cached data)" -fi -``` - -#### 3. Environment Configuration -**docker-compose.yml:** -```yaml -environment: - - MAX_DATA_AGE_DAYS=${MAX_DATA_AGE_DAYS:-7} -``` - -**.env.example:** -```bash -# Data Refresh Configuration -MAX_DATA_AGE_DAYS=7 # Refresh price data older than N days (0=always refresh) -``` - -### Data Flow - -1. **Container Startup** โ†’ entrypoint.sh begins execution -2. **Environment Validation** โ†’ Check required API keys (existing logic) -3. **Staleness Check** โ†’ `should_refresh_data()` scans `/app/data/daily_prices_*.json` - - No files found โ†’ Return 0 (refresh) - - Any file older than `MAX_DATA_AGE_DAYS` โ†’ Return 0 (refresh) - - All files fresh โ†’ Return 1 (skip) -4. **Conditional Fetch** โ†’ Run get_daily_price.py only if refresh needed -5. **Merge Data** โ†’ Always run merge_jsonl.py (handles missing merged.jsonl) -6. **MCP Services** โ†’ Start services (existing logic) -7. **Trading Agent** โ†’ Begin trading (existing logic) - -### Edge Cases - -| Scenario | Behavior | -|----------|----------| -| **First run (no data)** | Detects no files โ†’ triggers full fetch | -| **Restart within 7 days** | All files fresh โ†’ skips fetch (fast startup) | -| **Restart after 7 days** | Files stale โ†’ refreshes all data | -| **Partial data (some files missing)** | Missing files treated as infinitely old โ†’ triggers refresh | -| **Corrupt merged.jsonl but fresh price files** | Skips fetch, re-runs merge to rebuild merged.jsonl | -| **MAX_DATA_AGE_DAYS=0** | Always refresh (useful for testing/production) | -| **MAX_DATA_AGE_DAYS unset** | Defaults to 7 days | -| **Alpha Vantage rate limit** | get_daily_price.py handles with warning (existing behavior) | - -## Configuration Options - -| Variable | Default | Purpose | -|----------|---------|---------| -| `MAX_DATA_AGE_DAYS` | 7 | Days before price data considered stale | - -**Special Values:** -- `0` โ†’ Always refresh (force fresh data) -- `999` โ†’ Never refresh (use cached data indefinitely) - -## User Experience - -### Scenario 1: Fresh Container -``` -๐Ÿš€ Starting AI-Trader... -๐Ÿ” Validating environment variables... -โœ… Environment variables validated -๐Ÿ“Š Checking price data freshness... -๐Ÿ“ญ No price data found -๐Ÿ”„ Fetching and merging price data... -โœ“ Fetched NVDA -โœ“ Fetched MSFT -... -``` - -### Scenario 2: Restart Within 7 Days -``` -๐Ÿš€ Starting AI-Trader... -๐Ÿ” Validating environment variables... -โœ… Environment variables validated -๐Ÿ“Š Checking price data freshness... -โœ… All 103 price files are fresh (<7 days old) -โญ๏ธ Skipping data fetch (using cached data) -๐Ÿ”ง Starting MCP services... -``` - -### Scenario 3: Restart After 7 Days -``` -๐Ÿš€ Starting AI-Trader... -๐Ÿ” Validating environment variables... -โœ… Environment variables validated -๐Ÿ“Š Checking price data freshness... -๐Ÿ“… Found 103 stale files (>7 days old) -๐Ÿ”„ Fetching and merging price data... -โœ“ Fetched NVDA -โœ“ Fetched MSFT -... -``` - -## Testing Plan - -1. **Test fresh container:** Delete `./data/daily_prices_*.json`, start container โ†’ should fetch all -2. **Test cached data:** Restart immediately โ†’ should skip fetch -3. **Test staleness:** `touch -d "8 days ago" ./data/daily_prices_AAPL.json`, restart โ†’ should refresh -4. **Test partial data:** Delete 10 random price files โ†’ should refresh all -5. **Test MAX_DATA_AGE_DAYS=0:** Restart with env var set โ†’ should always fetch -6. **Test MAX_DATA_AGE_DAYS=30:** Restart with 8-day-old data โ†’ should skip - -## Documentation Updates - -Files requiring updates: -- `entrypoint.sh` โ†’ Add function and conditional logic -- `docker-compose.yml` โ†’ Add MAX_DATA_AGE_DAYS environment variable -- `.env.example` โ†’ Document MAX_DATA_AGE_DAYS with default value -- `CLAUDE.md` โ†’ Update "Docker Deployment" section with new env var -- `docs/DOCKER.md` (if exists) โ†’ Explain data caching behavior - -## Benefits - -- **Development:** Instant container restarts during iteration -- **API Quota:** ~103 fewer API calls per restart -- **Reliability:** No rate limit risks during frequent testing -- **Flexibility:** Configurable threshold for different use cases -- **Consistency:** Checks all files to ensure complete data diff --git a/docs/plans/2025-10-30-docker-deployment-design.md b/docs/plans/2025-10-30-docker-deployment-design.md deleted file mode 100644 index 45849e2..0000000 --- a/docs/plans/2025-10-30-docker-deployment-design.md +++ /dev/null @@ -1,491 +0,0 @@ -# Docker Deployment and CI/CD Design - -**Date:** 2025-10-30 -**Status:** Approved -**Target:** Development/local testing environment - -## Overview - -Package AI-Trader as a Docker container with docker-compose orchestration and automated image builds via GitHub Actions on release tags. Focus on simplicity and ease of use for researchers and developers. - -## Requirements - -- **Primary Use Case:** Development and local testing -- **Deployment Target:** Single monolithic container (all MCP services + trading agent) -- **Secrets Management:** Environment variables (no mounted .env file) -- **Data Strategy:** Fetch price data on container startup -- **Container Registry:** GitHub Container Registry (ghcr.io) -- **Trigger:** Build images automatically on release tag push (`v*` pattern) - -## Architecture - -### Components - -1. **Dockerfile** - Builds Python 3.10 image with all dependencies -2. **docker-compose.yml** - Orchestrates container with volume mounts and environment config -3. **entrypoint.sh** - Sequential startup script (data fetch โ†’ MCP services โ†’ trading agent) -4. **GitHub Actions Workflow** - Automated image build and push on release tags -5. **.dockerignore** - Excludes unnecessary files from image -6. **Documentation** - Docker usage guide and examples - -### Execution Flow - -``` -Container Start - โ†“ -entrypoint.sh - โ†“ -1. Fetch/merge price data (get_daily_price.py โ†’ merge_jsonl.py) - โ†“ -2. Start MCP services in background (start_mcp_services.py) - โ†“ -3. Wait 3 seconds for service stabilization - โ†“ -4. Run trading agent (main.py with config) - โ†“ -Container Exit โ†’ Cleanup MCP services -``` - -## Detailed Design - -### 1. Dockerfile - -**Multi-stage build:** - -```dockerfile -# Base stage -FROM python:3.10-slim as base - -WORKDIR /app - -# Install dependencies -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# Application stage -FROM base - -WORKDIR /app - -# Copy application code -COPY . . - -# Create necessary directories -RUN mkdir -p data logs data/agent_data - -# Make entrypoint executable -RUN chmod +x entrypoint.sh - -# Expose MCP service ports -EXPOSE 8000 8001 8002 8003 - -# Set Python to run unbuffered -ENV PYTHONUNBUFFERED=1 - -# Use entrypoint script -ENTRYPOINT ["./entrypoint.sh"] -CMD ["configs/default_config.json"] -``` - -**Key Features:** -- `python:3.10-slim` base for smaller image size -- Multi-stage for dependency caching -- Non-root user NOT included (dev/testing focus, can add later) -- Unbuffered Python output for real-time logs -- Default config path with override support - -### 2. docker-compose.yml - -```yaml -version: '3.8' - -services: - ai-trader: - build: . - container_name: ai-trader-app - volumes: - - ./data:/app/data - - ./logs:/app/logs - environment: - - OPENAI_API_BASE=${OPENAI_API_BASE} - - OPENAI_API_KEY=${OPENAI_API_KEY} - - ALPHAADVANTAGE_API_KEY=${ALPHAADVANTAGE_API_KEY} - - JINA_API_KEY=${JINA_API_KEY} - - RUNTIME_ENV_PATH=/app/data/runtime_env.json - - MATH_HTTP_PORT=${MATH_HTTP_PORT:-8000} - - SEARCH_HTTP_PORT=${SEARCH_HTTP_PORT:-8001} - - TRADE_HTTP_PORT=${TRADE_HTTP_PORT:-8002} - - GETPRICE_HTTP_PORT=${GETPRICE_HTTP_PORT:-8003} - - AGENT_MAX_STEP=${AGENT_MAX_STEP:-30} - ports: - - "8000:8000" - - "8001:8001" - - "8002:8002" - - "8003:8003" - - "8888:8888" # Optional: web dashboard - restart: unless-stopped -``` - -**Key Features:** -- Volume mounts for data/logs persistence -- Environment variables interpolated from `.env` file (Docker Compose reads automatically) -- No `.env` file mounted into container (cleaner separation) -- Default port values with override support -- Restart policy for recovery - -### 3. entrypoint.sh - -```bash -#!/bin/bash -set -e # Exit on any error - -echo "๐Ÿš€ Starting AI-Trader..." - -# Step 1: Data preparation -echo "๐Ÿ“Š Fetching and merging price data..." -cd /app/data -python get_daily_price.py -python merge_jsonl.py -cd /app - -# Step 2: Start MCP services in background -echo "๐Ÿ”ง Starting MCP services..." -cd /app/agent_tools -python start_mcp_services.py & -MCP_PID=$! -cd /app - -# Step 3: Wait for services to initialize -echo "โณ Waiting for MCP services to start..." -sleep 3 - -# Step 4: Run trading agent with config file -echo "๐Ÿค– Starting trading agent..." -CONFIG_FILE="${1:-configs/default_config.json}" -python main.py "$CONFIG_FILE" - -# Cleanup on exit -trap "echo '๐Ÿ›‘ Stopping MCP services...'; kill $MCP_PID 2>/dev/null" EXIT -``` - -**Key Features:** -- Sequential execution with clear logging -- MCP services run in background with PID capture -- Trap ensures cleanup on container exit -- Config file path as argument (defaults to `configs/default_config.json`) -- Fail-fast with `set -e` - -### 4. GitHub Actions Workflow - -**File:** `.github/workflows/docker-release.yml` - -```yaml -name: Build and Push Docker Image - -on: - push: - tags: - - 'v*' # Triggers on v1.0.0, v2.1.3, etc. - workflow_dispatch: # Manual trigger option - -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract version from tag - id: meta - run: | - VERSION=${GITHUB_REF#refs/tags/v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: | - ghcr.io/${{ github.repository_owner }}/ai-trader:${{ steps.meta.outputs.version }} - ghcr.io/${{ github.repository_owner }}/ai-trader:latest - cache-from: type=gha - cache-to: type=gha,mode=max -``` - -**Key Features:** -- Triggers on `v*` tags (e.g., `git tag v1.0.0 && git push origin v1.0.0`) -- Manual dispatch option for testing -- Uses `GITHUB_TOKEN` (automatically provided, no secrets needed) -- Builds with caching for faster builds -- Tags both version and `latest` -- Multi-platform support possible by adding `platforms: linux/amd64,linux/arm64` - -### 5. .dockerignore - -``` -# Version control -.git/ -.gitignore - -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -venv/ -env/ -ENV/ - -# IDE -.vscode/ -.idea/ -*.swp -*.swo - -# Environment and secrets -.env -.env.* -!.env.example - -# Data files (fetched at runtime) -data/*.json -data/agent_data/ -data/merged.jsonl - -# Logs -logs/ -*.log - -# Runtime state -runtime_env.json - -# Documentation (not needed in image) -*.md -docs/ -!README.md - -# CI/CD -.github/ -``` - -**Purpose:** -- Reduces image size -- Keeps secrets out of image -- Excludes generated files -- Keeps only necessary source code and scripts - -## Documentation Updates - -### New File: docs/DOCKER.md - -Create comprehensive Docker usage guide including: - -1. **Quick Start** - ```bash - cp .env.example .env - # Edit .env with your API keys - docker-compose up - ``` - -2. **Configuration** - - Required environment variables - - Optional configuration overrides - - Custom config file usage - -3. **Usage Examples** - ```bash - # Run with default config - docker-compose up - - # Run with custom config - docker-compose run ai-trader configs/my_config.json - - # View logs - docker-compose logs -f - - # Stop and clean up - docker-compose down - ``` - -4. **Data Persistence** - - How volume mounts work - - Where data is stored - - How to backup/restore - -5. **Troubleshooting** - - MCP services not starting โ†’ Check logs, verify ports available - - Missing API keys โ†’ Check .env file - - Data fetch failures โ†’ API rate limits or invalid keys - - Permission issues โ†’ Volume mount permissions - -6. **Using Pre-built Images** - ```bash - docker pull ghcr.io/hkuds/ai-trader:latest - docker run --env-file .env -v $(pwd)/data:/app/data ghcr.io/hkuds/ai-trader:latest - ``` - -### Update .env.example - -Add/clarify Docker-specific variables: - -```bash -# AI Model API Configuration -OPENAI_API_BASE=https://your-openai-proxy.com/v1 -OPENAI_API_KEY=your_openai_key - -# Data Source Configuration -ALPHAADVANTAGE_API_KEY=your_alpha_vantage_key -JINA_API_KEY=your_jina_api_key - -# System Configuration (Docker defaults) -RUNTIME_ENV_PATH=/app/data/runtime_env.json - -# MCP Service Ports -MATH_HTTP_PORT=8000 -SEARCH_HTTP_PORT=8001 -TRADE_HTTP_PORT=8002 -GETPRICE_HTTP_PORT=8003 - -# Agent Configuration -AGENT_MAX_STEP=30 -``` - -### Update Main README.md - -Add Docker section after "Quick Start": - -```markdown -## Docker Deployment - -### Using Docker Compose (Recommended) - -```bash -# Setup environment -cp .env.example .env -# Edit .env with your API keys - -# Run with docker-compose -docker-compose up -``` - -### Using Pre-built Images - -```bash -# Pull latest image -docker pull ghcr.io/hkuds/ai-trader:latest - -# Run container -docker run --env-file .env \ - -v $(pwd)/data:/app/data \ - -v $(pwd)/logs:/app/logs \ - ghcr.io/hkuds/ai-trader:latest -``` - -See [docs/DOCKER.md](docs/DOCKER.md) for detailed Docker usage guide. -``` - -## Release Process - -### For Maintainers - -1. **Prepare release:** - ```bash - # Ensure main branch is ready - git checkout main - git pull origin main - ``` - -2. **Create and push tag:** - ```bash - git tag v1.0.0 - git push origin v1.0.0 - ``` - -3. **GitHub Actions automatically:** - - Builds Docker image - - Tags with version and `latest` - - Pushes to `ghcr.io/hkuds/ai-trader` - -4. **Verify build:** - - Check Actions tab for build status - - Test pull: `docker pull ghcr.io/hkuds/ai-trader:v1.0.0` - -5. **Optional: Create GitHub Release** - - Add release notes - - Include Docker pull command - -### For Users - -```bash -# Pull specific version -docker pull ghcr.io/hkuds/ai-trader:v1.0.0 - -# Or always get latest -docker pull ghcr.io/hkuds/ai-trader:latest -``` - -## Implementation Checklist - -- [ ] Create Dockerfile with multi-stage build -- [ ] Create docker-compose.yml with volume mounts and environment config -- [ ] Create entrypoint.sh with sequential startup logic -- [ ] Create .dockerignore to exclude unnecessary files -- [ ] Create .github/workflows/docker-release.yml for CI/CD -- [ ] Create docs/DOCKER.md with comprehensive usage guide -- [ ] Update .env.example with Docker-specific variables -- [ ] Update main README.md with Docker deployment section -- [ ] Test local build: `docker-compose build` -- [ ] Test local run: `docker-compose up` -- [ ] Test with custom config -- [ ] Verify data persistence across container restarts -- [ ] Test GitHub Actions workflow (create test tag) -- [ ] Verify image pushed to ghcr.io -- [ ] Test pulling and running pre-built image -- [ ] Update CLAUDE.md with Docker commands - -## Future Enhancements - -Possible improvements for production use: - -1. **Multi-container Architecture** - - Separate containers for each MCP service - - Better isolation and independent scaling - - More complex orchestration - -2. **Security Hardening** - - Non-root user in container - - Docker secrets for production - - Read-only filesystem where possible - -3. **Monitoring** - - Health checks for MCP services - - Prometheus metrics export - - Logging aggregation - -4. **Optimization** - - Multi-platform builds (ARM64 support) - - Smaller base image (alpine) - - Layer caching optimization - -5. **Development Tools** - - docker-compose.dev.yml with hot reload - - Debug container with additional tools - - Integration test container - -These are deferred to keep initial implementation simple and focused on development/testing use cases. diff --git a/docs/plans/2025-10-30-docker-deployment.md b/docs/plans/2025-10-30-docker-deployment.md deleted file mode 100644 index 6d6eb78..0000000 --- a/docs/plans/2025-10-30-docker-deployment.md +++ /dev/null @@ -1,1183 +0,0 @@ -# Docker Deployment Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Package AI-Trader as a Docker container with docker-compose orchestration and automated CI/CD builds on release tags. - -**Architecture:** Single monolithic container running all MCP services and trading agent sequentially via entrypoint script. Environment variables injected via docker-compose. Price data fetched on startup. GitHub Actions builds and pushes images to ghcr.io on release tags. - -**Tech Stack:** Docker, Docker Compose, Bash, GitHub Actions, Python 3.10 - ---- - -## Task 1: Create .dockerignore - -**Files:** -- Create: `.dockerignore` - -**Step 1: Create .dockerignore file** - -Create `.dockerignore` in project root: - -``` -# Version control -.git/ -.gitignore - -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -venv/ -env/ -ENV/ -.venv/ - -# IDE -.vscode/ -.idea/ -*.swp -*.swo - -# Environment and secrets -.env -.env.* -!.env.example - -# Data files (fetched at runtime) -data/*.json -data/agent_data/ -data/merged.jsonl -data/merged_daily.jsonl -data/merged_hour.jsonl - -# Logs -logs/ -*.log - -# Runtime state -runtime_env.json -.runtime_env.json - -# Documentation (not needed in image) -docs/ -!README.md - -# CI/CD -.github/ - -# Git worktrees -.worktrees/ - -# Test files -test.py -delete.py -refresh_data.sh - -# Build artifacts -build/ -dist/ -*.egg-info/ -``` - -**Step 2: Verify file excludes data and secrets** - -Run: `cat .dockerignore | grep -E "\.env|data/.*\.json|\.git"` -Expected: Should show .env, data/*.json, .git/ lines - -**Step 3: Commit** - -```bash -git add .dockerignore -git commit -m "Add .dockerignore for Docker builds - -Excludes git history, Python cache, secrets, and runtime data" -``` - ---- - -## Task 2: Create Dockerfile - -**Files:** -- Create: `Dockerfile` - -**Step 1: Create Dockerfile with multi-stage build** - -Create `Dockerfile` in project root: - -```dockerfile -# Base stage - dependency installation -FROM python:3.10-slim as base - -WORKDIR /app - -# Install dependencies -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# Application stage -FROM base - -WORKDIR /app - -# Copy application code -COPY . . - -# Create necessary directories -RUN mkdir -p data logs data/agent_data - -# Make entrypoint executable -RUN chmod +x entrypoint.sh - -# Expose MCP service ports and web dashboard -EXPOSE 8000 8001 8002 8003 8888 - -# Set Python to run unbuffered for real-time logs -ENV PYTHONUNBUFFERED=1 - -# Use entrypoint script -ENTRYPOINT ["./entrypoint.sh"] -CMD ["configs/default_config.json"] -``` - -**Step 2: Verify Dockerfile syntax** - -Run: `docker build --dry-run . 2>&1 | grep -i error || echo "Syntax OK"` -Expected: "Syntax OK" (or no critical errors) - -**Step 3: Commit** - -```bash -git add Dockerfile -git commit -m "Add Dockerfile for containerization - -Multi-stage build with Python 3.10-slim base -Exposes MCP service ports and web dashboard -Uses entrypoint.sh for sequential startup" -``` - ---- - -## Task 3: Create entrypoint script - -**Files:** -- Create: `entrypoint.sh` - -**Step 1: Create entrypoint.sh with sequential startup logic** - -Create `entrypoint.sh` in project root: - -```bash -#!/bin/bash -set -e # Exit on any error - -echo "๐Ÿš€ Starting AI-Trader..." - -# Step 1: Data preparation -echo "๐Ÿ“Š Fetching and merging price data..." -cd /app/data -python get_daily_price.py -python merge_jsonl.py -cd /app - -# Step 2: Start MCP services in background -echo "๐Ÿ”ง Starting MCP services..." -cd /app/agent_tools -python start_mcp_services.py & -MCP_PID=$! -cd /app - -# Step 3: Wait for services to initialize -echo "โณ Waiting for MCP services to start..." -sleep 3 - -# Step 4: Run trading agent with config file -echo "๐Ÿค– Starting trading agent..." -CONFIG_FILE="${1:-configs/default_config.json}" -python main.py "$CONFIG_FILE" - -# Cleanup on exit -trap "echo '๐Ÿ›‘ Stopping MCP services...'; kill $MCP_PID 2>/dev/null; exit 0" EXIT SIGTERM SIGINT -``` - -**Step 2: Make script executable** - -Run: `chmod +x entrypoint.sh` -Expected: No output - -**Step 3: Verify script has executable permissions** - -Run: `ls -la entrypoint.sh | grep -E "^-rwxr"` -Expected: Shows executable permissions (x flags) - -**Step 4: Commit** - -```bash -git add entrypoint.sh -git commit -m "Add entrypoint script for container startup - -Sequential execution: data fetch โ†’ MCP services โ†’ trading agent -Handles graceful shutdown of background services -Supports custom config file as argument" -``` - ---- - -## Task 4: Create docker-compose.yml - -**Files:** -- Create: `docker-compose.yml` - -**Step 1: Create docker-compose.yml with service definition** - -Create `docker-compose.yml` in project root: - -```yaml -version: '3.8' - -services: - ai-trader: - build: . - container_name: ai-trader-app - volumes: - - ./data:/app/data - - ./logs:/app/logs - environment: - # AI Model API Configuration - - OPENAI_API_BASE=${OPENAI_API_BASE} - - OPENAI_API_KEY=${OPENAI_API_KEY} - - # Data Source Configuration - - ALPHAADVANTAGE_API_KEY=${ALPHAADVANTAGE_API_KEY} - - JINA_API_KEY=${JINA_API_KEY} - - # System Configuration - - RUNTIME_ENV_PATH=/app/data/runtime_env.json - - # MCP Service Ports - - MATH_HTTP_PORT=${MATH_HTTP_PORT:-8000} - - SEARCH_HTTP_PORT=${SEARCH_HTTP_PORT:-8001} - - TRADE_HTTP_PORT=${TRADE_HTTP_PORT:-8002} - - GETPRICE_HTTP_PORT=${GETPRICE_HTTP_PORT:-8003} - - # Agent Configuration - - AGENT_MAX_STEP=${AGENT_MAX_STEP:-30} - ports: - - "8000:8000" - - "8001:8001" - - "8002:8002" - - "8003:8003" - - "8888:8888" - restart: unless-stopped -``` - -**Step 2: Validate YAML syntax** - -Run: `docker-compose config 2>&1 | grep -i error || echo "YAML valid"` -Expected: "YAML valid" (or docker-compose shows parsed config) - -**Step 3: Commit** - -```bash -git add docker-compose.yml -git commit -m "Add docker-compose configuration - -Mounts data and logs volumes for persistence -Injects environment variables from .env file -Exposes all MCP service ports and web dashboard -Auto-restart on failure" -``` - ---- - -## Task 5: Update .env.example for Docker - -**Files:** -- Modify: `.env.example` - -**Step 1: Read current .env.example** - -Run: `cat .env.example` -Expected: Shows existing environment variable examples - -**Step 2: Add Docker-specific documentation to .env.example** - -Add these lines at the top of `.env.example` (or update existing variables): - -```bash -# ============================================================================= -# AI-Trader Environment Configuration -# ============================================================================= -# Copy this file to .env and fill in your actual values -# Docker Compose automatically reads .env from project root - -# AI Model API Configuration -OPENAI_API_BASE=https://your-openai-proxy.com/v1 -OPENAI_API_KEY=your_openai_key_here - -# Data Source Configuration -ALPHAADVANTAGE_API_KEY=your_alphavantage_key_here -JINA_API_KEY=your_jina_key_here - -# System Configuration (Docker default paths) -RUNTIME_ENV_PATH=/app/data/runtime_env.json - -# MCP Service Ports (defaults shown) -MATH_HTTP_PORT=8000 -SEARCH_HTTP_PORT=8001 -TRADE_HTTP_PORT=8002 -GETPRICE_HTTP_PORT=8003 - -# Agent Configuration -AGENT_MAX_STEP=30 -``` - -**Step 3: Verify .env.example has all required variables** - -Run: `grep -E "OPENAI_API_KEY|ALPHAADVANTAGE_API_KEY|JINA_API_KEY" .env.example` -Expected: Shows all three API key variables - -**Step 4: Commit** - -```bash -git add .env.example -git commit -m "Update .env.example with Docker configuration - -Add Docker-specific paths and documentation -Include all required API keys and MCP ports -Show default values for optional settings" -``` - ---- - -## Task 6: Create Docker documentation - -**Files:** -- Create: `docs/DOCKER.md` - -**Step 1: Create docs/DOCKER.md with comprehensive usage guide** - -Create `docs/DOCKER.md`: - -```markdown -# Docker Deployment Guide - -## Quick Start - -### Prerequisites -- Docker Engine 20.10+ -- Docker Compose 2.0+ -- API keys for OpenAI, Alpha Vantage, and Jina AI - -### First-Time Setup - -1. **Clone repository:** - ```bash - git clone https://github.com/HKUDS/AI-Trader.git - cd AI-Trader - ``` - -2. **Configure environment:** - ```bash - cp .env.example .env - # Edit .env and add your API keys - ``` - -3. **Run with Docker Compose:** - ```bash - docker-compose up - ``` - -That's it! The container will: -- Fetch latest price data from Alpha Vantage -- Start all MCP services -- Run the trading agent with default configuration - -## Configuration - -### Environment Variables - -Edit `.env` file with your credentials: - -```bash -# Required -OPENAI_API_KEY=sk-... -ALPHAADVANTAGE_API_KEY=... -JINA_API_KEY=... - -# Optional (defaults shown) -MATH_HTTP_PORT=8000 -SEARCH_HTTP_PORT=8001 -TRADE_HTTP_PORT=8002 -GETPRICE_HTTP_PORT=8003 -AGENT_MAX_STEP=30 -``` - -### Custom Trading Configuration - -Pass a custom config file: - -```bash -docker-compose run ai-trader configs/my_config.json -``` - -## Usage Examples - -### Run in foreground with logs -```bash -docker-compose up -``` - -### Run in background (detached) -```bash -docker-compose up -d -docker-compose logs -f # Follow logs -``` - -### Run with custom config -```bash -docker-compose run ai-trader configs/custom_config.json -``` - -### Stop containers -```bash -docker-compose down -``` - -### Rebuild after code changes -```bash -docker-compose build -docker-compose up -``` - -## Data Persistence - -### Volume Mounts - -Docker Compose mounts two volumes: - -- `./data:/app/data` - Price data and trading records -- `./logs:/app/logs` - MCP service logs - -Data persists across container restarts. To reset: - -```bash -docker-compose down -rm -rf data/agent_data/* logs/* -docker-compose up -``` - -### Backup Trading Data - -```bash -# Backup -tar -czf ai-trader-backup-$(date +%Y%m%d).tar.gz data/agent_data/ - -# Restore -tar -xzf ai-trader-backup-YYYYMMDD.tar.gz -``` - -## Using Pre-built Images - -### Pull from GitHub Container Registry - -```bash -docker pull ghcr.io/hkuds/ai-trader:latest -``` - -### Run without Docker Compose - -```bash -docker run --env-file .env \ - -v $(pwd)/data:/app/data \ - -v $(pwd)/logs:/app/logs \ - -p 8000-8003:8000-8003 \ - ghcr.io/hkuds/ai-trader:latest -``` - -### Specific version -```bash -docker pull ghcr.io/hkuds/ai-trader:v1.0.0 -``` - -## Troubleshooting - -### MCP Services Not Starting - -**Symptom:** Container exits immediately or errors about ports - -**Solutions:** -- Check ports 8000-8003 not already in use: `lsof -i :8000-8003` -- View container logs: `docker-compose logs` -- Check MCP service logs: `cat logs/math.log` - -### Missing API Keys - -**Symptom:** Errors about missing environment variables - -**Solutions:** -- Verify `.env` file exists: `ls -la .env` -- Check required variables set: `grep OPENAI_API_KEY .env` -- Ensure `.env` in same directory as docker-compose.yml - -### Data Fetch Failures - -**Symptom:** Container exits during data preparation step - -**Solutions:** -- Verify Alpha Vantage API key valid -- Check API rate limits (5 requests/minute for free tier) -- View logs: `docker-compose logs | grep "Fetching and merging"` - -### Permission Issues - -**Symptom:** Cannot write to data or logs directories - -**Solutions:** -- Ensure directories writable: `chmod -R 755 data logs` -- Check volume mount permissions -- May need to create directories first: `mkdir -p data logs` - -### Container Keeps Restarting - -**Symptom:** Container restarts repeatedly - -**Solutions:** -- View logs to identify error: `docker-compose logs --tail=50` -- Disable auto-restart: Comment out `restart: unless-stopped` in docker-compose.yml -- Check if main.py exits with error - -## Advanced Usage - -### Override Entrypoint - -Run bash inside container for debugging: - -```bash -docker-compose run --entrypoint /bin/bash ai-trader -``` - -### Build Multi-platform Images - -For ARM64 (Apple Silicon) and AMD64: - -```bash -docker buildx build --platform linux/amd64,linux/arm64 -t ai-trader . -``` - -### View Container Resource Usage - -```bash -docker stats ai-trader-app -``` - -### Access MCP Services Directly - -Services exposed on host: -- Math: http://localhost:8000 -- Search: http://localhost:8001 -- Trade: http://localhost:8002 -- Price: http://localhost:8003 - -## Development Workflow - -### Local Code Changes - -1. Edit code in project root -2. Rebuild image: `docker-compose build` -3. Run updated container: `docker-compose up` - -### Test Different Configurations - -```bash -# Create test config -cp configs/default_config.json configs/test_config.json -# Edit test_config.json - -# Run with test config -docker-compose run ai-trader configs/test_config.json -``` - -## Production Deployment - -For production use, consider: - -1. **Use specific version tags** instead of `latest` -2. **External secrets management** (AWS Secrets Manager, etc.) -3. **Health checks** in docker-compose.yml -4. **Resource limits** (CPU/memory) -5. **Log aggregation** (ELK stack, CloudWatch) -6. **Orchestration** (Kubernetes, Docker Swarm) - -See design document in `docs/plans/2025-10-30-docker-deployment-design.md` for architecture details. -``` - -**Step 2: Verify markdown formatting** - -Run: `head -20 docs/DOCKER.md` -Expected: Shows properly formatted markdown header - -**Step 3: Commit** - -```bash -git add docs/DOCKER.md -git commit -m "Add Docker deployment documentation - -Comprehensive guide including: -- Quick start instructions -- Configuration options -- Usage examples and volume persistence -- Troubleshooting common issues -- Pre-built image usage" -``` - ---- - -## Task 7: Update main README with Docker section - -**Files:** -- Modify: `README.md` - -**Step 1: Read current README to find insertion point** - -Run: `grep -n "## ๐Ÿš€ Quick Start" README.md` -Expected: Shows line number of Quick Start section - -**Step 2: Add Docker section after Quick Start** - -Insert this content after the "## ๐Ÿš€ Quick Start" section (around line 210): - -```markdown -## ๐Ÿณ Docker Deployment - -### Using Docker Compose (Recommended) - -The easiest way to run AI-Trader is with Docker Compose: - -```bash -# 1. Clone and setup -git clone https://github.com/HKUDS/AI-Trader.git -cd AI-Trader - -# 2. Configure environment -cp .env.example .env -# Edit .env with your API keys: -# - OPENAI_API_KEY -# - ALPHAADVANTAGE_API_KEY -# - JINA_API_KEY - -# 3. Run with Docker Compose -docker-compose up -``` - -The container automatically: -- Fetches latest NASDAQ 100 price data -- Starts all MCP services -- Runs AI trading agents - -### Using Pre-built Images - -Pull and run pre-built images from GitHub Container Registry: - -```bash -# Pull latest version -docker pull ghcr.io/hkuds/ai-trader:latest - -# Run container -docker run --env-file .env \ - -v $(pwd)/data:/app/data \ - -v $(pwd)/logs:/app/logs \ - ghcr.io/hkuds/ai-trader:latest -``` - -**๐Ÿ“– See [docs/DOCKER.md](docs/DOCKER.md) for detailed Docker usage, troubleshooting, and advanced configuration.** - ---- -``` - -**Step 3: Verify Docker section added** - -Run: `grep -A 5 "## ๐Ÿณ Docker Deployment" README.md` -Expected: Shows the Docker section content - -**Step 4: Commit** - -```bash -git add README.md -git commit -m "Add Docker deployment section to README - -Include quick start with Docker Compose -Add pre-built image usage instructions -Link to detailed Docker documentation" -``` - ---- - -## Task 8: Create GitHub Actions workflow - -**Files:** -- Create: `.github/workflows/docker-release.yml` - -**Step 1: Create .github/workflows directory** - -Run: `mkdir -p .github/workflows` -Expected: No output - -**Step 2: Create docker-release.yml workflow** - -Create `.github/workflows/docker-release.yml`: - -```yaml -name: Build and Push Docker Image - -on: - push: - tags: - - 'v*' # Triggers on v1.0.0, v2.1.3, etc. - workflow_dispatch: # Manual trigger option - -jobs: - build-and-push: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract version from tag - id: meta - run: | - VERSION=${GITHUB_REF#refs/tags/v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Building version: $VERSION" - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: | - ghcr.io/${{ github.repository_owner }}/ai-trader:${{ steps.meta.outputs.version }} - ghcr.io/${{ github.repository_owner }}/ai-trader:latest - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Image published - run: | - echo "โœ… Docker image published successfully!" - echo "๐Ÿ“ฆ Pull with: docker pull ghcr.io/${{ github.repository_owner }}/ai-trader:${{ steps.meta.outputs.version }}" - echo "๐Ÿ“ฆ Or latest: docker pull ghcr.io/${{ github.repository_owner }}/ai-trader:latest" -``` - -**Step 3: Verify YAML syntax** - -Run: `cat .github/workflows/docker-release.yml | grep -E "name:|on:|jobs:" | head -3` -Expected: Shows workflow name, triggers, and jobs - -**Step 4: Commit** - -```bash -git add .github/workflows/docker-release.yml -git commit -m "Add GitHub Actions workflow for Docker builds - -Triggers on release tags (v*) and manual dispatch -Builds and pushes to GitHub Container Registry -Tags with both version and latest -Uses build caching for faster builds" -``` - ---- - -## Task 9: Update CLAUDE.md with Docker commands - -**Files:** -- Modify: `CLAUDE.md` - -**Step 1: Add Docker section to CLAUDE.md development commands** - -Insert after the "### Starting Services" section in CLAUDE.md: - -```markdown -### Docker Deployment - -```bash -# Build Docker image -docker-compose build - -# Run with Docker Compose -docker-compose up - -# Run in background -docker-compose up -d - -# Run with custom config -docker-compose run ai-trader configs/my_config.json - -# View logs -docker-compose logs -f - -# Stop and remove containers -docker-compose down - -# Pull pre-built image -docker pull ghcr.io/hkuds/ai-trader:latest - -# Test local Docker build -docker build -t ai-trader-test . -docker run --env-file .env -v $(pwd)/data:/app/data ai-trader-test -``` - -### Releasing Docker Images - -```bash -# Create and push release tag -git tag v1.0.0 -git push origin v1.0.0 - -# GitHub Actions automatically: -# 1. Builds Docker image -# 2. Tags with version and latest -# 3. Pushes to ghcr.io/hkuds/ai-trader - -# Verify build in Actions tab -# https://github.com/HKUDS/AI-Trader/actions -``` -``` - -**Step 2: Verify Docker commands added** - -Run: `grep -A 10 "### Docker Deployment" CLAUDE.md` -Expected: Shows Docker commands section - -**Step 3: Commit** - -```bash -git add CLAUDE.md -git commit -m "Update CLAUDE.md with Docker commands - -Add Docker build and run commands -Include release process for Docker images -Document GitHub Actions automation" -``` - ---- - -## Task 10: Test Docker build locally - -**Files:** -- None (verification task) - -**Step 1: Build Docker image** - -Run: `docker-compose build` -Expected: Build completes successfully, shows "Successfully built" and image ID - -**Step 2: Verify image created** - -Run: `docker images | grep ai-trader` -Expected: Shows ai-trader image with size and creation time - -**Step 3: Test dry-run (without API keys)** - -Create minimal test .env: -```bash -cat > .env.test << 'EOF' -OPENAI_API_KEY=test -ALPHAADVANTAGE_API_KEY=test -JINA_API_KEY=test -RUNTIME_ENV_PATH=/app/data/runtime_env.json -EOF -``` - -Run: `docker-compose --env-file .env.test config` -Expected: Shows parsed configuration without errors - -**Step 4: Document test results** - -Create file `docs/plans/docker-test-results.txt` with output from build - -**Step 5: Commit test documentation** - -```bash -git add docs/plans/docker-test-results.txt -git commit -m "Add Docker build test results - -Local build verification completed successfully -Image builds without errors -Configuration parses correctly" -``` - ---- - -## Task 11: Create release checklist documentation - -**Files:** -- Create: `docs/RELEASING.md` - -**Step 1: Create release process documentation** - -Create `docs/RELEASING.md`: - -```markdown -# Release Process - -## Creating a New Release - -### 1. Prepare Release - -1. Ensure `main` branch is stable and tests pass -2. Update version numbers if needed -3. Update CHANGELOG.md with release notes - -### 2. Create Release Tag - -```bash -# Ensure on main branch -git checkout main -git pull origin main - -# Create annotated tag -git tag -a v1.0.0 -m "Release v1.0.0: Docker deployment support" - -# Push tag to trigger CI/CD -git push origin v1.0.0 -``` - -### 3. GitHub Actions Automation - -Tag push automatically triggers `.github/workflows/docker-release.yml`: - -1. โœ… Checks out code -2. โœ… Sets up Docker Buildx -3. โœ… Logs into GitHub Container Registry -4. โœ… Extracts version from tag -5. โœ… Builds Docker image with caching -6. โœ… Pushes to `ghcr.io/hkuds/ai-trader:VERSION` -7. โœ… Pushes to `ghcr.io/hkuds/ai-trader:latest` - -### 4. Verify Build - -1. Check GitHub Actions: https://github.com/HKUDS/AI-Trader/actions -2. Verify workflow completed successfully (green checkmark) -3. Check packages: https://github.com/HKUDS/AI-Trader/pkgs/container/ai-trader - -### 5. Test Release - -```bash -# Pull released image -docker pull ghcr.io/hkuds/ai-trader:v1.0.0 - -# Test run -docker run --env-file .env \ - -v $(pwd)/data:/app/data \ - ghcr.io/hkuds/ai-trader:v1.0.0 -``` - -### 6. Create GitHub Release (Optional) - -1. Go to https://github.com/HKUDS/AI-Trader/releases/new -2. Select tag: `v1.0.0` -3. Release title: `v1.0.0 - Docker Deployment Support` -4. Add release notes: - -```markdown -## ๐Ÿณ Docker Deployment - -This release adds full Docker support for easy deployment. - -### Pull and Run - -```bash -docker pull ghcr.io/hkuds/ai-trader:v1.0.0 -docker run --env-file .env -v $(pwd)/data:/app/data ghcr.io/hkuds/ai-trader:v1.0.0 -``` - -Or use Docker Compose: - -```bash -docker-compose up -``` - -See [docs/DOCKER.md](docs/DOCKER.md) for details. - -### What's New -- Docker containerization with single-container architecture -- docker-compose.yml for easy orchestration -- Automated CI/CD builds on release tags -- Pre-built images on GitHub Container Registry -``` - -5. Publish release - -## Version Numbering - -Use Semantic Versioning (SEMVER): - -- `v1.0.0` - Major release (breaking changes) -- `v1.1.0` - Minor release (new features, backward compatible) -- `v1.1.1` - Patch release (bug fixes) - -## Troubleshooting Releases - -### Build Fails in GitHub Actions - -1. Check Actions logs for error details -2. Test local build: `docker build .` -3. Fix issues and delete/recreate tag: - -```bash -# Delete tag -git tag -d v1.0.0 -git push origin :refs/tags/v1.0.0 - -# Recreate after fixes -git tag v1.0.0 -git push origin v1.0.0 -``` - -### Image Not Appearing in Registry - -1. Check Actions permissions (Settings โ†’ Actions โ†’ General) -2. Verify `packages: write` permission in workflow -3. Ensure `GITHUB_TOKEN` has registry access - -### Wrong Version Tagged - -Delete and recreate: - -```bash -git tag -d v1.0.0 -git push origin :refs/tags/v1.0.0 -git tag v1.0.1 -git push origin v1.0.1 -``` - -## Manual Build and Push - -If automated build fails, manual push: - -```bash -# Build locally -docker build -t ghcr.io/hkuds/ai-trader:v1.0.0 . - -# Login to GHCR -echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin - -# Push -docker push ghcr.io/hkuds/ai-trader:v1.0.0 -docker tag ghcr.io/hkuds/ai-trader:v1.0.0 ghcr.io/hkuds/ai-trader:latest -docker push ghcr.io/hkuds/ai-trader:latest -``` -``` - -**Step 2: Verify release documentation complete** - -Run: `grep -E "^## |^### " docs/RELEASING.md` -Expected: Shows all section headers - -**Step 3: Commit** - -```bash -git add docs/RELEASING.md -git commit -m "Add release process documentation - -Complete guide for creating releases: -- Tag creation and push process -- GitHub Actions automation workflow -- Verification and testing steps -- Troubleshooting common issues" -``` - ---- - -## Task 12: Final validation and cleanup - -**Files:** -- None (validation task) - -**Step 1: Verify all Docker files created** - -Run: -```bash -ls -la Dockerfile docker-compose.yml .dockerignore entrypoint.sh .github/workflows/docker-release.yml -``` -Expected: All files exist with correct permissions - -**Step 2: Verify documentation complete** - -Run: -```bash -ls -la docs/DOCKER.md docs/RELEASING.md docs/plans/2025-10-30-docker-deployment-design.md -``` -Expected: All documentation files exist - -**Step 3: Verify git status clean** - -Run: `git status` -Expected: Shows "working tree clean" or only untracked .env files - -**Step 4: Review commit history** - -Run: `git log --oneline -15` -Expected: Shows all 11+ commits from this implementation - -**Step 5: Create summary commit if needed** - -If any files uncommitted: -```bash -git add -A -git commit -m "Docker deployment implementation complete - -All components implemented: -- Dockerfile with multi-stage build -- docker-compose.yml with volume mounts -- entrypoint.sh for sequential startup -- GitHub Actions workflow for releases -- Comprehensive documentation -- .dockerignore for clean builds" -``` - ---- - -## Testing Checklist - -Before merging to main: - -- [ ] Docker image builds successfully (`docker-compose build`) -- [ ] Image size reasonable (<500MB for base image) -- [ ] Container starts without errors (with test .env) -- [ ] MCP services start in container -- [ ] Volume mounts work (data/logs persist) -- [ ] Custom config file can be passed -- [ ] Documentation accurate and complete -- [ ] GitHub Actions workflow syntax valid -- [ ] .dockerignore excludes unnecessary files - -## Post-Implementation - -After merging: - -1. Create test tag to verify GitHub Actions: `git tag v0.1.0-test` -2. Monitor Actions build -3. Test pulling from ghcr.io -4. Update project README.md if needed -5. Announce Docker support to users - ---- - -**Implementation Complete!** All Docker deployment components created with comprehensive documentation and automated CI/CD. diff --git a/docs/plans/docker-test-results.txt b/docs/plans/docker-test-results.txt deleted file mode 100644 index 7a1c4ca..0000000 --- a/docs/plans/docker-test-results.txt +++ /dev/null @@ -1,102 +0,0 @@ -Docker Build Test Results -========================== -Date: 2025-10-30 -Branch: docker-deployment -Working Directory: /home/bballou/AI-Trader/.worktrees/docker-deployment - -Test 1: Docker Image Build ---------------------------- -Command: docker-compose build -Status: SUCCESS -Result: Successfully built image 7b36b8f4c0e9 - -Build Output Summary: -- Base image: python:3.10-slim -- Build stages: Multi-stage build (base + application) -- Dependencies installed successfully from requirements.txt -- Application code copied -- Directories created: data, logs, data/agent_data -- Entrypoint script made executable -- Ports exposed: 8000, 8001, 8002, 8003, 8888 -- Environment: PYTHONUNBUFFERED=1 set -- Image size: 266MB -- Build time: ~2 minutes (including dependency installation) - -Key packages installed: -- langchain==1.0.2 -- langchain-openai==1.0.1 -- langchain-mcp-adapters>=0.1.0 -- fastmcp==2.12.5 -- langgraph<1.1.0,>=1.0.0 -- pydantic<3.0.0,>=2.7.4 -- openai<3.0.0,>=1.109.1 -- All dependencies resolved without conflicts - -Test 2: Image Verification ---------------------------- -Command: docker images | grep ai-trader -Status: SUCCESS -Result: docker-deployment_ai-trader latest 7b36b8f4c0e9 9 seconds ago 266MB - -Image Details: -- Repository: docker-deployment_ai-trader -- Tag: latest -- Image ID: 7b36b8f4c0e9 -- Created: Just now -- Size: 266MB (reasonable for Python 3.10 + ML dependencies) - -Test 3: Configuration Parsing (Dry-Run) ----------------------------------------- -Command: docker-compose --env-file .env.test config -Status: SUCCESS -Result: Configuration parsed correctly without errors - -Test .env.test contents: -OPENAI_API_KEY=test -ALPHAADVANTAGE_API_KEY=test -JINA_API_KEY=test -RUNTIME_ENV_PATH=/app/data/runtime_env.json - -Parsed Configuration: -- Service name: ai-trader -- Container name: ai-trader-app -- Build context: /home/bballou/AI-Trader/.worktrees/docker-deployment -- Environment variables correctly injected: - * AGENT_MAX_STEP: '30' (default) - * ALPHAADVANTAGE_API_KEY: test - * GETPRICE_HTTP_PORT: '8003' (default) - * JINA_API_KEY: test - * MATH_HTTP_PORT: '8000' (default) - * OPENAI_API_BASE: '' (not set, defaulted to blank) - * OPENAI_API_KEY: test - * RUNTIME_ENV_PATH: /app/data/runtime_env.json - * SEARCH_HTTP_PORT: '8001' (default) - * TRADE_HTTP_PORT: '8002' (default) -- Ports correctly mapped: 8000, 8001, 8002, 8003, 8888 -- Volumes correctly configured: - * ./data:/app/data:rw - * ./logs:/app/logs:rw -- Restart policy: unless-stopped -- Docker Compose version: 3.8 - -Summary -------- -All Docker build tests PASSED successfully: -โœ“ Docker image builds without errors -โœ“ Image created with reasonable size (266MB) -โœ“ Multi-stage build optimizes layer caching -โœ“ All Python dependencies install correctly -โœ“ Configuration parsing works with test environment -โœ“ Environment variables properly injected -โœ“ Volume mounts configured correctly -โœ“ Port mappings set up correctly -โœ“ Restart policy configured - -No issues encountered during local Docker build testing. -The Docker deployment is ready for use. - -Next Steps: -1. Test actual container startup with valid API keys -2. Verify MCP services start correctly in container -3. Test trading agent execution -4. Consider creating test tag for GitHub Actions CI/CD verification diff --git a/docs/reference/data-formats.md b/docs/reference/data-formats.md new file mode 100644 index 0000000..2ae44af --- /dev/null +++ b/docs/reference/data-formats.md @@ -0,0 +1,30 @@ +# Data Formats + +File formats and schemas used by AI-Trader. + +--- + +## Position File (`position.jsonl`) + +```jsonl +{"date": "2025-01-16", "id": 1, "this_action": {"action": "buy", "symbol": "AAPL", "amount": 10}, "positions": {"AAPL": 10, "CASH": 9500.0}} +{"date": "2025-01-17", "id": 2, "this_action": {"action": "sell", "symbol": "AAPL", "amount": 5}, "positions": {"AAPL": 5, "CASH": 10750.0}} +``` + +--- + +## Price Data (`merged.jsonl`) + +```jsonl +{"Meta Data": {"2. Symbol": "AAPL", "3. Last Refreshed": "2025-01-16"}, "Time Series (Daily)": {"2025-01-16": {"1. buy price": "250.50", "2. high": "252.00", "3. low": "249.00", "4. sell price": "251.50", "5. volume": "50000000"}}} +``` + +--- + +## Log Files (`log.jsonl`) + +Contains complete AI reasoning and tool usage for each trading session. + +--- + +See database schema in [docs/developer/database-schema.md](../developer/database-schema.md) for SQLite formats. diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md new file mode 100644 index 0000000..3ca1e2c --- /dev/null +++ b/docs/reference/environment-variables.md @@ -0,0 +1,32 @@ +# Environment Variables Reference + +Complete list of configuration variables. + +--- + +See [docs/user-guide/configuration.md](../user-guide/configuration.md#environment-variables) for detailed descriptions. + +--- + +## Required + +- `OPENAI_API_KEY` +- `ALPHAADVANTAGE_API_KEY` +- `JINA_API_KEY` + +--- + +## Optional + +- `API_PORT` (default: 8080) +- `API_HOST` (default: 0.0.0.0) +- `OPENAI_API_BASE` +- `MAX_CONCURRENT_JOBS` (default: 1) +- `MAX_SIMULATION_DAYS` (default: 30) +- `AUTO_DOWNLOAD_PRICE_DATA` (default: true) +- `AGENT_MAX_STEP` (default: 30) +- `VOLUME_PATH` (default: .) +- `MATH_HTTP_PORT` (default: 8000) +- `SEARCH_HTTP_PORT` (default: 8001) +- `TRADE_HTTP_PORT` (default: 8002) +- `GETPRICE_HTTP_PORT` (default: 8003) diff --git a/docs/reference/mcp-tools.md b/docs/reference/mcp-tools.md new file mode 100644 index 0000000..5b87f86 --- /dev/null +++ b/docs/reference/mcp-tools.md @@ -0,0 +1,39 @@ +# MCP Tools Reference + +Model Context Protocol tools available to AI agents. + +--- + +## Available Tools + +### Math Tool (Port 8000) +Mathematical calculations and analysis. + +### Search Tool (Port 8001) +Market intelligence via Jina AI search. +- News articles +- Analyst reports +- Financial data + +### Trade Tool (Port 8002) +Buy/sell execution. +- Place orders +- Check balances +- View positions + +### Price Tool (Port 8003) +Historical and current price data. +- OHLCV data +- Multiple symbols +- Date filtering + +--- + +## Usage + +AI agents access tools automatically through MCP protocol. +Tools are localhost-only and not exposed to external network. + +--- + +See `agent_tools/` directory for implementations. diff --git a/docs/testing-specification.md b/docs/testing-specification.md deleted file mode 100644 index 7fab38d..0000000 --- a/docs/testing-specification.md +++ /dev/null @@ -1,1155 +0,0 @@ -# Comprehensive Testing Suite Specification - -## 1. Overview - -This document defines the complete testing strategy, test suite structure, coverage requirements, and quality thresholds for the AI-Trader API service. - -**Testing Philosophy:** -- **Test-Driven Development (TDD)** for critical paths -- **High coverage** (โ‰ฅ85%) for production code -- **Fast feedback** - unit tests run in <10 seconds -- **Realistic integration tests** with test database -- **Performance benchmarks** to catch regressions -- **Security testing** for API vulnerabilities - ---- - -## 2. Testing Thresholds & Requirements - -### 2.1 Code Coverage - -| Component | Minimum Coverage | Target Coverage | Notes | -|-----------|-----------------|-----------------|-------| -| **api/job_manager.py** | 90% | 95% | Critical - job lifecycle | -| **api/worker.py** | 85% | 90% | Core execution logic | -| **api/executor.py** | 85% | 90% | Model-day execution | -| **api/results_service.py** | 90% | 95% | Data retrieval | -| **api/database.py** | 95% | 100% | Database utilities | -| **api/runtime_manager.py** | 85% | 90% | Config isolation | -| **api/main.py** | 80% | 85% | API endpoints | -| **Overall** | **85%** | **90%** | **Project minimum** | - -**Enforcement:** -- CI/CD pipeline **fails** if coverage drops below minimum -- Coverage report generated on every commit -- Uncovered lines flagged in PR reviews - ---- - -### 2.2 Performance Thresholds - -| Metric | Threshold | Test Method | -|--------|-----------|-------------| -| **Unit test suite** | < 10 seconds | `pytest tests/unit/` | -| **Integration test suite** | < 60 seconds | `pytest tests/integration/` | -| **API endpoint `/simulate/trigger`** | < 500ms | Load testing | -| **API endpoint `/simulate/status`** | < 100ms | Load testing | -| **API endpoint `/results?detail=minimal`** | < 200ms | Load testing | -| **API endpoint `/results?detail=full`** | < 1 second | Load testing | -| **Database query (get_job)** | < 50ms | Benchmark tests | -| **Database query (get_job_progress)** | < 100ms | Benchmark tests | -| **Simulation (single model-day)** | 30-60s | Acceptance test | - -**Enforcement:** -- Performance tests run nightly -- Alerts triggered if thresholds exceeded -- Benchmark results tracked over time - ---- - -### 2.3 Quality Gates - -**All PRs must pass:** -1. โœ… All tests passing (unit + integration) -2. โœ… Code coverage โ‰ฅ 85% -3. โœ… No critical security vulnerabilities (Bandit scan) -4. โœ… Linting passes (Ruff or Flake8) -5. โœ… Type checking passes (mypy with strict mode) -6. โœ… No performance regressions (ยฑ10% tolerance) - -**Release checklist:** -1. โœ… All quality gates pass -2. โœ… End-to-end tests pass in Docker -3. โœ… Load testing passes (100 concurrent requests) -4. โœ… Security scan passes (OWASP ZAP) -5. โœ… Manual smoke tests complete - ---- - -## 3. Test Suite Structure - -``` -tests/ -โ”œโ”€โ”€ __init__.py -โ”œโ”€โ”€ conftest.py # Shared pytest fixtures -โ”‚ -โ”œโ”€โ”€ unit/ # Fast, isolated tests -โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”œโ”€โ”€ test_job_manager.py # JobManager CRUD operations -โ”‚ โ”œโ”€โ”€ test_database.py # Database utilities -โ”‚ โ”œโ”€โ”€ test_runtime_manager.py # Config isolation -โ”‚ โ”œโ”€โ”€ test_results_service.py # Results queries -โ”‚ โ””โ”€โ”€ test_models.py # Pydantic model validation -โ”‚ -โ”œโ”€โ”€ integration/ # Tests with real dependencies -โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”œโ”€โ”€ test_api_endpoints.py # FastAPI endpoint tests -โ”‚ โ”œโ”€โ”€ test_worker.py # Job execution workflow -โ”‚ โ”œโ”€โ”€ test_executor.py # Model-day execution -โ”‚ โ””โ”€โ”€ test_end_to_end.py # Complete simulation flow -โ”‚ -โ”œโ”€โ”€ performance/ # Benchmark and load tests -โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”œโ”€โ”€ test_database_benchmarks.py -โ”‚ โ”œโ”€โ”€ test_api_load.py # Locust or pytest-benchmark -โ”‚ โ””โ”€โ”€ test_simulation_timing.py -โ”‚ -โ”œโ”€โ”€ security/ # Security tests -โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ”œโ”€โ”€ test_api_security.py # Input validation, injection -โ”‚ โ””โ”€โ”€ test_auth.py # Future: API key validation -โ”‚ -โ””โ”€โ”€ e2e/ # End-to-end with Docker - โ”œโ”€โ”€ __init__.py - โ””โ”€โ”€ test_docker_workflow.py # Full Docker compose scenario -``` - ---- - -## 4. Unit Tests - -### 4.1 test_job_manager.py - -```python -# tests/unit/test_job_manager.py - -import pytest -import tempfile -import os -from datetime import datetime, timedelta -from api.job_manager import JobManager - -@pytest.fixture -def job_manager(): - """Create JobManager with temporary database""" - temp_db = tempfile.NamedTemporaryFile(delete=False, suffix=".db") - temp_db.close() - - jm = JobManager(db_path=temp_db.name) - yield jm - - # Cleanup - os.unlink(temp_db.name) - - -class TestJobCreation: - """Test job creation and validation""" - - def test_create_job_success(self, job_manager): - """Should create job with pending status""" - job_id = job_manager.create_job( - config_path="configs/test.json", - date_range=["2025-01-16", "2025-01-17"], - models=["gpt-5", "claude-3.7-sonnet"] - ) - - assert job_id is not None - job = job_manager.get_job(job_id) - assert job["status"] == "pending" - assert job["date_range"] == ["2025-01-16", "2025-01-17"] - assert job["models"] == ["gpt-5", "claude-3.7-sonnet"] - assert job["created_at"] is not None - - def test_create_job_with_job_details(self, job_manager): - """Should create job_details for each model-day""" - job_id = job_manager.create_job( - config_path="configs/test.json", - date_range=["2025-01-16", "2025-01-17"], - models=["gpt-5"] - ) - - progress = job_manager.get_job_progress(job_id) - assert progress["total_model_days"] == 2 # 2 dates ร— 1 model - assert progress["completed"] == 0 - assert progress["failed"] == 0 - - def test_create_job_blocks_concurrent(self, job_manager): - """Should prevent creating second job while first is pending""" - job1_id = job_manager.create_job( - "configs/test.json", - ["2025-01-16"], - ["gpt-5"] - ) - - with pytest.raises(ValueError, match="Another simulation job is already running"): - job_manager.create_job( - "configs/test.json", - ["2025-01-17"], - ["gpt-5"] - ) - - def test_create_job_after_completion(self, job_manager): - """Should allow new job after previous completes""" - job1_id = job_manager.create_job( - "configs/test.json", - ["2025-01-16"], - ["gpt-5"] - ) - - job_manager.update_job_status(job1_id, "completed") - - # Now second job should be allowed - job2_id = job_manager.create_job( - "configs/test.json", - ["2025-01-17"], - ["gpt-5"] - ) - assert job2_id is not None - - -class TestJobStatusTransitions: - """Test job status state machine""" - - def test_pending_to_running(self, job_manager): - """Should transition from pending to running""" - job_id = job_manager.create_job( - "configs/test.json", - ["2025-01-16"], - ["gpt-5"] - ) - - # Update detail to running - job_manager.update_job_detail_status(job_id, "2025-01-16", "gpt-5", "running") - - job = job_manager.get_job(job_id) - assert job["status"] == "running" - assert job["started_at"] is not None - - def test_running_to_completed(self, job_manager): - """Should transition to completed when all details complete""" - job_id = job_manager.create_job( - "configs/test.json", - ["2025-01-16"], - ["gpt-5"] - ) - - job_manager.update_job_detail_status(job_id, "2025-01-16", "gpt-5", "running") - job_manager.update_job_detail_status(job_id, "2025-01-16", "gpt-5", "completed") - - job = job_manager.get_job(job_id) - assert job["status"] == "completed" - assert job["completed_at"] is not None - assert job["total_duration_seconds"] is not None - - def test_partial_completion(self, job_manager): - """Should mark as partial when some models fail""" - job_id = job_manager.create_job( - "configs/test.json", - ["2025-01-16"], - ["gpt-5", "claude-3.7-sonnet"] - ) - - # First model succeeds - job_manager.update_job_detail_status(job_id, "2025-01-16", "gpt-5", "running") - job_manager.update_job_detail_status(job_id, "2025-01-16", "gpt-5", "completed") - - # Second model fails - job_manager.update_job_detail_status(job_id, "2025-01-16", "claude-3.7-sonnet", "running") - job_manager.update_job_detail_status( - job_id, "2025-01-16", "claude-3.7-sonnet", "failed", - error="API timeout" - ) - - job = job_manager.get_job(job_id) - assert job["status"] == "partial" - - progress = job_manager.get_job_progress(job_id) - assert progress["completed"] == 1 - assert progress["failed"] == 1 - - -class TestJobRetrieval: - """Test job query operations""" - - def test_get_nonexistent_job(self, job_manager): - """Should return None for nonexistent job""" - job = job_manager.get_job("nonexistent-id") - assert job is None - - def test_get_current_job(self, job_manager): - """Should return most recent job""" - job1_id = job_manager.create_job("configs/test.json", ["2025-01-16"], ["gpt-5"]) - job_manager.update_job_status(job1_id, "completed") - - job2_id = job_manager.create_job("configs/test.json", ["2025-01-17"], ["gpt-5"]) - - current = job_manager.get_current_job() - assert current["job_id"] == job2_id - - def test_find_job_by_date_range(self, job_manager): - """Should find existing job with same date range""" - job_id = job_manager.create_job( - "configs/test.json", - ["2025-01-16", "2025-01-17"], - ["gpt-5"] - ) - - found = job_manager.find_job_by_date_range(["2025-01-16", "2025-01-17"]) - assert found["job_id"] == job_id - - -class TestJobProgress: - """Test job progress tracking""" - - def test_progress_all_pending(self, job_manager): - """Should show 0 completed when all pending""" - job_id = job_manager.create_job( - "configs/test.json", - ["2025-01-16", "2025-01-17"], - ["gpt-5"] - ) - - progress = job_manager.get_job_progress(job_id) - assert progress["total_model_days"] == 2 - assert progress["completed"] == 0 - assert progress["failed"] == 0 - assert progress["current"] is None - - def test_progress_with_running(self, job_manager): - """Should identify currently running model-day""" - job_id = job_manager.create_job( - "configs/test.json", - ["2025-01-16"], - ["gpt-5"] - ) - - job_manager.update_job_detail_status(job_id, "2025-01-16", "gpt-5", "running") - - progress = job_manager.get_job_progress(job_id) - assert progress["current"] == {"date": "2025-01-16", "model": "gpt-5"} - - def test_progress_details(self, job_manager): - """Should return detailed progress for all model-days""" - job_id = job_manager.create_job( - "configs/test.json", - ["2025-01-16"], - ["gpt-5", "claude-3.7-sonnet"] - ) - - job_manager.update_job_detail_status(job_id, "2025-01-16", "gpt-5", "completed") - - progress = job_manager.get_job_progress(job_id) - assert len(progress["details"]) == 2 - assert progress["details"][0]["model"] == "gpt-5" - assert progress["details"][0]["status"] == "completed" - - -class TestJobCleanup: - """Test maintenance operations""" - - def test_cleanup_old_jobs(self, job_manager): - """Should delete jobs older than threshold""" - # Create old job (manually set created_at) - from api.database import get_db_connection - conn = get_db_connection(job_manager.db_path) - cursor = conn.cursor() - - old_date = (datetime.utcnow() - timedelta(days=35)).isoformat() + "Z" - cursor.execute(""" - INSERT INTO jobs (job_id, config_path, status, date_range, models, created_at) - VALUES (?, ?, ?, ?, ?, ?) - """, ("old-job", "configs/test.json", "completed", '["2025-01-01"]', '["gpt-5"]', old_date)) - conn.commit() - conn.close() - - # Create recent job - recent_id = job_manager.create_job("configs/test.json", ["2025-01-16"], ["gpt-5"]) - - # Cleanup jobs older than 30 days - deleted = job_manager.cleanup_old_jobs(days=30) - - assert deleted["jobs_deleted"] == 1 - assert job_manager.get_job("old-job") is None - assert job_manager.get_job(recent_id) is not None - - -# ========== Coverage Target: 95% for job_manager.py ========== -``` - ---- - -### 4.2 test_results_service.py - -```python -# tests/unit/test_results_service.py - -import pytest -import tempfile -import os -from api.results_service import ResultsService -from api.database import get_db_connection - -@pytest.fixture -def results_service(): - """Create ResultsService with test data""" - temp_db = tempfile.NamedTemporaryFile(delete=False, suffix=".db") - temp_db.close() - - service = ResultsService(db_path=temp_db.name) - - # Populate test data - _populate_test_data(temp_db.name) - - yield service - - os.unlink(temp_db.name) - - -def _populate_test_data(db_path): - """Insert sample positions data""" - from api.database import initialize_database - initialize_database(db_path) - - conn = get_db_connection(db_path) - cursor = conn.cursor() - - # Insert sample job - cursor.execute(""" - INSERT INTO jobs (job_id, config_path, status, date_range, models, created_at) - VALUES (?, ?, ?, ?, ?, ?) - """, ("test-job", "configs/test.json", "completed", '["2025-01-16"]', '["gpt-5"]', "2025-01-16T00:00:00Z")) - - # Insert positions - cursor.execute(""" - INSERT INTO positions ( - job_id, date, model, action_id, action_type, symbol, amount, price, - cash, portfolio_value, daily_profit, daily_return_pct, - cumulative_profit, cumulative_return_pct, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ("test-job", "2025-01-16", "gpt-5", 1, "buy", "AAPL", 10, 255.88, - 7441.2, 10000.0, 0.0, 0.0, 0.0, 0.0, "2025-01-16T09:30:00Z")) - - position_id = cursor.lastrowid - - # Insert holdings - cursor.execute(""" - INSERT INTO holdings (position_id, symbol, quantity) - VALUES (?, ?, ?) - """, (position_id, "AAPL", 10)) - - conn.commit() - conn.close() - - -class TestGetResults: - """Test results retrieval""" - - def test_get_results_minimal(self, results_service): - """Should return minimal results for date""" - results = results_service.get_results("2025-01-16", model="gpt-5", detail="minimal") - - assert results["date"] == "2025-01-16" - assert len(results["results"]) == 1 - assert results["results"][0]["model"] == "gpt-5" - assert "AAPL" in results["results"][0]["positions"] - assert results["results"][0]["positions"]["AAPL"] == 10 - assert results["results"][0]["positions"]["CASH"] == 7441.2 - - def test_get_results_nonexistent_date(self, results_service): - """Should return empty results for nonexistent date""" - results = results_service.get_results("2099-12-31", model="gpt-5") - assert results["results"] == [] - - def test_get_results_all_models(self, results_service): - """Should return all models when model not specified""" - results = results_service.get_results("2025-01-16") - assert len(results["results"]) >= 1 # At least one model - - -class TestPortfolioTimeseries: - """Test timeseries queries""" - - def test_get_timeseries(self, results_service): - """Should return portfolio values over time""" - timeseries = results_service.get_portfolio_timeseries("gpt-5") - - assert len(timeseries) >= 1 - assert timeseries[0]["date"] == "2025-01-16" - assert "portfolio_value" in timeseries[0] - - def test_get_timeseries_with_date_range(self, results_service): - """Should filter by date range""" - timeseries = results_service.get_portfolio_timeseries( - "gpt-5", - start_date="2025-01-16", - end_date="2025-01-16" - ) - - assert len(timeseries) == 1 - - -class TestLeaderboard: - """Test leaderboard generation""" - - def test_get_leaderboard(self, results_service): - """Should rank models by portfolio value""" - leaderboard = results_service.get_leaderboard() - - assert len(leaderboard) >= 1 - assert leaderboard[0]["rank"] == 1 - assert "portfolio_value" in leaderboard[0] - - def test_leaderboard_for_specific_date(self, results_service): - """Should generate leaderboard for specific date""" - leaderboard = results_service.get_leaderboard(date="2025-01-16") - assert len(leaderboard) >= 1 - - -# ========== Coverage Target: 95% for results_service.py ========== -``` - ---- - -## 5. Integration Tests - -### 5.1 test_api_endpoints.py - -```python -# tests/integration/test_api_endpoints.py - -import pytest -from fastapi.testclient import TestClient -from api.main import app -import tempfile -import os - -@pytest.fixture -def client(): - """Create test client with temporary database""" - temp_db = tempfile.NamedTemporaryFile(delete=False, suffix=".db") - temp_db.close() - - # Override database path for testing - os.environ["TEST_DB_PATH"] = temp_db.name - - client = TestClient(app) - yield client - - os.unlink(temp_db.name) - - -class TestTriggerEndpoint: - """Test /simulate/trigger endpoint""" - - def test_trigger_simulation_success(self, client): - """Should accept simulation trigger and return job_id""" - response = client.post("/simulate/trigger", json={ - "config_path": "configs/test.json" - }) - - assert response.status_code == 202 - data = response.json() - assert "job_id" in data - assert data["status"] == "accepted" - assert "date_range" in data - assert "models" in data - - def test_trigger_simulation_already_running(self, client): - """Should return existing job if already running""" - # First request - response1 = client.post("/simulate/trigger", json={ - "config_path": "configs/test.json" - }) - job_id_1 = response1.json()["job_id"] - - # Second request (before first completes) - response2 = client.post("/simulate/trigger", json={ - "config_path": "configs/test.json" - }) - - # Should return same job_id - assert response2.status_code in (200, 202) - # job_id_2 = response2.json()["job_id"] - # assert job_id_1 == job_id_2 # TODO: Fix based on actual implementation - - def test_trigger_simulation_invalid_config(self, client): - """Should return 400 for invalid config path""" - response = client.post("/simulate/trigger", json={ - "config_path": "nonexistent.json" - }) - - assert response.status_code == 400 - - -class TestStatusEndpoint: - """Test /simulate/status/{job_id} endpoint""" - - def test_get_status_success(self, client): - """Should return job status""" - # Create job first - trigger_response = client.post("/simulate/trigger", json={ - "config_path": "configs/test.json" - }) - job_id = trigger_response.json()["job_id"] - - # Get status - response = client.get(f"/simulate/status/{job_id}") - - assert response.status_code == 200 - data = response.json() - assert data["job_id"] == job_id - assert data["status"] in ("pending", "running", "completed", "partial", "failed") - assert "progress" in data - - def test_get_status_nonexistent(self, client): - """Should return 404 for nonexistent job""" - response = client.get("/simulate/status/nonexistent-id") - assert response.status_code == 404 - - -class TestResultsEndpoint: - """Test /results endpoint""" - - def test_get_results_success(self, client): - """Should return simulation results""" - # TODO: Populate test data first - response = client.get("/results", params={ - "date": "2025-01-16", - "model": "gpt-5", - "detail": "minimal" - }) - - # May be 404 if no data, or 200 if test data exists - assert response.status_code in (200, 404) - - def test_get_results_invalid_date(self, client): - """Should return 400 for invalid date format""" - response = client.get("/results", params={ - "date": "invalid-date" - }) - - assert response.status_code == 400 - - -class TestHealthEndpoint: - """Test /health endpoint""" - - def test_health_check(self, client): - """Should return healthy status""" - response = client.get("/health") - - assert response.status_code in (200, 503) # May be 503 if MCP services not running - data = response.json() - assert "status" in data - assert "services" in data - - -# ========== Coverage Target: 85% for main.py ========== -``` - ---- - -## 6. Performance Tests - -```python -# tests/performance/test_api_load.py - -import pytest -from locust import HttpUser, task, between -import time - -class AITraderAPIUser(HttpUser): - """Simulate API user load""" - wait_time = between(1, 3) # Wait 1-3 seconds between requests - - @task(3) - def get_health(self): - """Most common endpoint""" - self.client.get("/health") - - @task(2) - def get_results(self): - """Fetch results""" - self.client.get("/results?date=2025-01-16&model=gpt-5&detail=minimal") - - @task(1) - def trigger_simulation(self): - """Less common - trigger simulation""" - self.client.post("/simulate/trigger", json={ - "config_path": "configs/test.json" - }) - - -# Run with: locust -f tests/performance/test_api_load.py --host=http://localhost:8080 -``` - -```python -# tests/performance/test_database_benchmarks.py - -import pytest -from api.job_manager import JobManager -import time - -@pytest.mark.benchmark -def test_create_job_performance(benchmark, job_manager): - """Benchmark job creation time""" - result = benchmark( - job_manager.create_job, - "configs/test.json", - ["2025-01-16"], - ["gpt-5"] - ) - - # Should complete in < 50ms - assert benchmark.stats.mean < 0.05 - - -@pytest.mark.benchmark -def test_get_job_performance(benchmark, job_manager): - """Benchmark job retrieval time""" - # Create job first - job_id = job_manager.create_job("configs/test.json", ["2025-01-16"], ["gpt-5"]) - - result = benchmark(job_manager.get_job, job_id) - - # Should complete in < 10ms - assert benchmark.stats.mean < 0.01 - - -# Run with: pytest tests/performance/ --benchmark-only -``` - ---- - -## 7. Security Tests - -```python -# tests/security/test_api_security.py - -import pytest -from fastapi.testclient import TestClient -from api.main import app - -client = TestClient(app) - - -class TestInputValidation: - """Test input validation and sanitization""" - - def test_sql_injection_protection(self): - """Should reject SQL injection attempts""" - response = client.get("/results", params={ - "date": "2025-01-16' OR '1'='1", - "model": "gpt-5" - }) - - # Should return 400 (invalid date format), not execute SQL - assert response.status_code == 400 - - def test_path_traversal_protection(self): - """Should reject path traversal attempts""" - response = client.post("/simulate/trigger", json={ - "config_path": "../../etc/passwd" - }) - - # Should reject or return 404 - assert response.status_code in (400, 404) - - def test_xss_protection(self): - """Should sanitize XSS attempts""" - response = client.post("/simulate/trigger", json={ - "config_path": "" - }) - - assert response.status_code in (400, 404) - # Response should not contain unsanitized script - assert "