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:
-
-
-
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 "