Compare commits

..

7 Commits

Author SHA1 Message Date
a16bac5d08 fix: use 'args' instead of 'arguments' in MCPToolCallRequest
MCPToolCallRequest has 'args' attribute, not 'arguments'. Fixed
attribute name to match the actual API.
2025-11-02 20:21:43 -05:00
81b92e293a fix: make ContextInjector async to match ToolCallInterceptor protocol
The interceptor __call__ method must be async and follow the proper
signature: async __call__(request, handler) -> result

Previous implementation was synchronous and had wrong signature, causing
'object function can't be used in await expression' error.
2025-11-02 20:11:20 -05:00
b1b486dcc4 fix: inject signature and today_date into trade tool calls for concurrent simulations
Resolves issue where MCP trade tools couldn't access SIGNATURE and TODAY_DATE
during concurrent API simulations, causing "SIGNATURE environment variable is
not set" errors.

Problem:
- MCP services run as separate HTTP processes
- Multiple simulations execute concurrently via ThreadPoolExecutor
- Environment variables from executor process not accessible to MCP services

Solution:
- Add ContextInjector that implements ToolCallInterceptor
- Automatically injects signature and today_date into buy/sell tool calls
- Trade tools accept optional parameters, falling back to config/env
- BaseAgent creates interceptor and updates today_date per session

Changes:
- agent/context_injector.py: New interceptor for context injection
- agent/base_agent/base_agent.py: Create and use ContextInjector
- agent_tools/tool_trade.py: Add optional signature/today_date parameters

Benefits:
- Supports concurrent multi-model simulations
- Maintains backward compatibility with CLI mode
- AI model unaware of injected parameters
2025-11-02 20:01:32 -05:00
1bdfefae35 refactor: remove duplicate MCP service log files
Remove redundant log file creation for MCP services since output is
already captured by Docker logs. This simplifies deployment by removing
unnecessary volume mounts and file management.

Changes:
- Remove logs/ directory creation from Dockerfile
- Remove logs/ volume mount from docker-compose.yml
- Update start_mcp_services.py to send output to DEVNULL
- Update documentation to reflect changes (DOCKER.md, docs/DOCKER.md)
- Update .env.example to remove logs/ from volume description

Users can still view MCP service output via 'docker logs' or
'docker-compose logs -f'. Trading session logs in data/agent_data/
remain unchanged.
2025-11-02 19:57:17 -05:00
dbd8f0141c fix: initialize agent properly in API mode
Fixed critical bug where ModelDayExecutor._initialize_agent() created
a BaseAgent but never called agent.initialize(), leaving self.model=None.
This caused 'NoneType' object has no attribute 'bind' error when
run_trading_session() tried to create the langchain agent.

Changes:
- Made _initialize_agent() async
- Added await agent.initialize() call
- Updated call site to await async function

Now properly initializes MCP client, tools, and AI model before
executing trading sessions, matching the working CLI mode pattern.
2025-11-02 19:39:43 -05:00
fb32bb12c5 fix: check column existence before creating indexes
Fixes startup error 'no such column: session_id' that occurs when
_create_indexes() tries to create indexes on columns that don't exist yet.

The issue occurred when initializing a database from scratch:
1. _migrate_schema() adds session_id column to positions table
2. _create_indexes() tries to create index on session_id
3. But on fresh databases, positions table was created without session_id
4. Migration runs after table creation, before index creation
5. Index creation fails because column doesn't exist yet

Solution: Check if columns exist before creating indexes on them.

This ensures the database can be initialized both:
- Fresh (CREATE TABLE without session_id, then ALTER TABLE, then CREATE INDEX)
- Migrated (ALTER TABLE adds column, then CREATE INDEX)

Tested: All 21 database tests passing
2025-11-02 19:24:19 -05:00
29af5ddb4c fix: remove non-existent data scripts from Dockerfile
The get_daily_price.py, get_interdaily_price.py, and merge_jsonl.py
scripts don't exist in the repository. Removed the RUN command that
tries to copy these files to /app/scripts/.

This fixes the Docker build failure:
ERROR: process did not complete successfully: exit code: 1

The API server doesn't need these data preparation scripts as it
operates on existing database records.
2025-11-02 19:18:12 -05:00
11 changed files with 169 additions and 101 deletions

View File

@@ -35,7 +35,7 @@ MAX_SIMULATION_DAYS=30
AUTO_DOWNLOAD_PRICE_DATA=true
# Data Volume Configuration
# Base directory for all persistent data (will contain data/, logs/, configs/ subdirectories)
# Base directory for all persistent data (will contain data/ and configs/ subdirectories)
# Use relative paths (./volumes) or absolute paths (/home/user/ai-trader-volumes)
# Defaults to current directory (.) if not set
VOLUME_PATH=.

View File

@@ -154,10 +154,9 @@ docker-compose up
### Volume Mounts
Docker Compose mounts three volumes for persistent data. By default, these are stored in the project directory:
Docker Compose mounts two volumes for persistent data. By default, these are stored in the project directory:
- `./data:/app/data` - Price data and trading records
- `./logs:/app/logs` - MCP service logs
- `./configs:/app/configs` - Configuration files (allows editing configs without rebuilding)
### Custom Volume Location
@@ -174,7 +173,6 @@ VOLUME_PATH=./volumes
This will store data in:
- `/home/user/trading-data/data/`
- `/home/user/trading-data/logs/`
- `/home/user/trading-data/configs/`
**Note:** The directory structure is automatically created. You'll need to copy your existing configs:
@@ -190,7 +188,7 @@ To reset all trading data:
```bash
docker-compose down
rm -rf ${VOLUME_PATH:-.}/data/agent_data/* ${VOLUME_PATH:-.}/logs/*
rm -rf ${VOLUME_PATH:-.}/data/agent_data/*
docker-compose up
```
@@ -217,8 +215,7 @@ docker pull ghcr.io/xe138/ai-trader-server:latest
```bash
docker run --env-file .env \
-v $(pwd)/data:/app/data \
-v $(pwd)/logs:/app/logs \
-p 8000-8003:8000-8003 \
-p 8080:8080 \
ghcr.io/xe138/ai-trader-server:latest
```
@@ -234,9 +231,9 @@ docker pull ghcr.io/xe138/ai-trader-server:v1.0.0
**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`
- Check if API port 8080 is already in use: `lsof -i :8080`
- Verify MCP services started by checking Docker logs for service startup messages
### Missing API Keys
@@ -258,12 +255,12 @@ docker pull ghcr.io/xe138/ai-trader-server:v1.0.0
### Permission Issues
**Symptom:** Cannot write to data or logs directories
**Symptom:** Cannot write to data directory
**Solutions:**
- Ensure directories writable: `chmod -R 755 data logs`
- Ensure data directory is writable: `chmod -R 755 data`
- Check volume mount permissions
- May need to create directories first: `mkdir -p data logs`
- May need to create directory first: `mkdir -p data`
### Container Keeps Restarting
@@ -298,13 +295,12 @@ docker buildx build --platform linux/amd64,linux/arm64 -t ai-trader-server .
docker stats ai-trader-server
```
### Access MCP Services Directly
### Access API Directly
Services exposed on host:
- Math: http://localhost:8000
- Search: http://localhost:8001
- Trade: http://localhost:8002
- Price: http://localhost:8003
API server exposed on host:
- REST API: http://localhost:8080
MCP services run internally and are not exposed to the host.
## Development Workflow

View File

@@ -26,14 +26,8 @@ WORKDIR /app
# Copy application code
COPY . .
# Copy data scripts to separate directory (volume mount won't overlay these)
RUN mkdir -p /app/scripts && \
cp data/get_daily_price.py /app/scripts/ && \
cp data/get_interdaily_price.py /app/scripts/ && \
cp data/merge_jsonl.py /app/scripts/
# Create necessary directories
RUN mkdir -p data logs data/agent_data
RUN mkdir -p data data/agent_data
# Make entrypoint executable
RUN chmod +x entrypoint.sh

View File

@@ -29,6 +29,7 @@ from tools.deployment_config import (
log_api_key_warning,
get_deployment_mode
)
from agent.context_injector import ContextInjector
# Load environment variables
load_dotenv()
@@ -124,6 +125,9 @@ class BaseAgent:
self.tools: Optional[List] = None
self.model: Optional[ChatOpenAI] = None
self.agent: Optional[Any] = None
# Context injector for MCP tools
self.context_injector: Optional[ContextInjector] = None
# Data paths
self.data_path = os.path.join(self.base_log_path, self.signature)
@@ -169,16 +173,27 @@ class BaseAgent:
print("⚠️ OpenAI base URL not set, using default")
try:
# Create MCP client
self.client = MultiServerMCPClient(self.mcp_config)
# Create context injector for injecting signature and today_date into tool calls
self.context_injector = ContextInjector(
signature=self.signature,
today_date=self.init_date # Will be updated per trading session
)
# Create MCP client with interceptor
self.client = MultiServerMCPClient(
self.mcp_config,
tool_interceptors=[self.context_injector]
)
# Get tools
self.tools = await self.client.get_tools()
if not self.tools:
raw_tools = await self.client.get_tools()
if not raw_tools:
print("⚠️ Warning: No MCP tools loaded. MCP services may not be running.")
print(f" MCP configuration: {self.mcp_config}")
self.tools = []
else:
print(f"✅ Loaded {len(self.tools)} MCP tools")
print(f"✅ Loaded {len(raw_tools)} MCP tools")
self.tools = raw_tools
except Exception as e:
raise RuntimeError(
f"❌ Failed to initialize MCP client: {e}\n"
@@ -336,6 +351,10 @@ Summary:"""
"""
print(f"📈 Starting trading session: {today_date}")
# Update context injector with current trading date
if self.context_injector:
self.context_injector.today_date = today_date
# Clear conversation history for new trading day
self.clear_conversation_history()

55
agent/context_injector.py Normal file
View File

@@ -0,0 +1,55 @@
"""
Tool interceptor for injecting runtime context into MCP tool calls.
This interceptor automatically injects `signature` and `today_date` parameters
into buy/sell tool calls to support concurrent multi-model simulations.
"""
from typing import Any, Callable, Awaitable
class ContextInjector:
"""
Intercepts tool calls to inject runtime context (signature, today_date).
Usage:
interceptor = ContextInjector(signature="gpt-5", today_date="2025-10-01")
client = MultiServerMCPClient(config, tool_interceptors=[interceptor])
"""
def __init__(self, signature: str, today_date: str):
"""
Initialize context injector.
Args:
signature: Model signature to inject
today_date: Trading date to inject
"""
self.signature = signature
self.today_date = today_date
async def __call__(
self,
request: Any, # MCPToolCallRequest
handler: Callable[[Any], Awaitable[Any]]
) -> Any: # MCPToolCallResult
"""
Intercept tool call and inject context parameters.
Args:
request: Tool call request containing name and arguments
handler: Async callable to execute the actual tool
Returns:
Result from handler after injecting context
"""
# Inject signature and today_date for trade tools
if request.name in ["buy", "sell"]:
# Add signature and today_date to args if not present
if "signature" not in request.args:
request.args["signature"] = self.signature
if "today_date" not in request.args:
request.args["today_date"] = self.today_date
# Call the actual tool handler
return await handler(request)

View File

@@ -52,10 +52,6 @@ class MCPServiceManager:
}
}
# Create logs directory
self.log_dir = Path('logs')
self.log_dir.mkdir(exist_ok=True)
# Set signal handlers
signal.signal(signal.SIGINT, self.signal_handler)
signal.signal(signal.SIGTERM, self.signal_handler)
@@ -77,27 +73,23 @@ class MCPServiceManager:
return False
try:
# Start service process
log_file = self.log_dir / f"{service_id}.log"
# Set PYTHONPATH to /app so services can import from tools module
env = os.environ.copy()
env['PYTHONPATH'] = str(Path.cwd())
with open(log_file, 'w') as f:
process = subprocess.Popen(
[sys.executable, str(script_path)],
stdout=f,
stderr=subprocess.STDOUT,
cwd=Path.cwd(), # Use current working directory (/app)
env=env # Pass environment with PYTHONPATH
)
# Start service process (output goes to Docker logs)
process = subprocess.Popen(
[sys.executable, str(script_path)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=Path.cwd(), # Use current working directory (/app)
env=env # Pass environment with PYTHONPATH
)
self.services[service_id] = {
'process': process,
'name': service_name,
'port': port,
'log_file': log_file
'port': port
}
print(f"{service_name} service started (PID: {process.pid}, Port: {port})")
@@ -167,15 +159,14 @@ class MCPServiceManager:
print(f"{service['name']} service running normally")
else:
print(f"{service['name']} service failed to start")
print(f" Please check logs: {service['log_file']}")
print(f" Check Docker logs for details: docker logs ai-trader-server")
def print_service_info(self):
"""Print service information"""
print("\n📋 Service information:")
for service_id, service in self.services.items():
print(f" - {service['name']}: http://localhost:{service['port']} (PID: {service['process'].pid})")
print(f"\n📁 Log files location: {self.log_dir.absolute()}")
print("\n🛑 Press Ctrl+C to stop all services")
def keep_alive(self):

View File

@@ -13,41 +13,45 @@ mcp = FastMCP("TradeTools")
@mcp.tool()
def buy(symbol: str, amount: int) -> Dict[str, Any]:
def buy(symbol: str, amount: int, signature: str = None, today_date: str = None) -> Dict[str, Any]:
"""
Buy stock function
This function simulates stock buying operations, including the following steps:
1. Get current position and operation ID
2. Get stock opening price for the day
3. Validate buy conditions (sufficient cash)
4. Update position (increase stock quantity, decrease cash)
5. Record transaction to position.jsonl file
Args:
symbol: Stock symbol, such as "AAPL", "MSFT", etc.
amount: Buy quantity, must be a positive integer, indicating how many shares to buy
signature: Model signature (optional, will use config/env if not provided)
today_date: Trading date (optional, will use config/env if not provided)
Returns:
Dict[str, Any]:
- Success: Returns new position dictionary (containing stock quantity and cash balance)
- Failure: Returns {"error": error message, ...} dictionary
Raises:
ValueError: Raised when SIGNATURE environment variable is not set
Example:
>>> result = buy("AAPL", 10)
>>> print(result) # {"AAPL": 110, "MSFT": 5, "CASH": 5000.0, ...}
"""
# Step 1: Get environment variables and basic information
# Get signature (model name) from environment variable, used to determine data storage path
signature = get_config_value("SIGNATURE")
# Get signature (model name) from parameter or fallback to config/env
if signature is None:
raise ValueError("SIGNATURE environment variable is not set")
# Get current trading date from environment variable
today_date = get_config_value("TODAY_DATE")
signature = get_config_value("SIGNATURE")
if signature is None:
raise ValueError("SIGNATURE not provided and environment variable is not set")
# Get current trading date from parameter or fallback to config/env
if today_date is None:
today_date = get_config_value("TODAY_DATE")
# Step 2: Get current latest position and operation ID
# get_latest_position returns two values: position dictionary and current maximum operation ID
@@ -104,41 +108,45 @@ def buy(symbol: str, amount: int) -> Dict[str, Any]:
return new_position
@mcp.tool()
def sell(symbol: str, amount: int) -> Dict[str, Any]:
def sell(symbol: str, amount: int, signature: str = None, today_date: str = None) -> Dict[str, Any]:
"""
Sell stock function
This function simulates stock selling operations, including the following steps:
1. Get current position and operation ID
2. Get stock opening price for the day
3. Validate sell conditions (position exists, sufficient quantity)
4. Update position (decrease stock quantity, increase cash)
5. Record transaction to position.jsonl file
Args:
symbol: Stock symbol, such as "AAPL", "MSFT", etc.
amount: Sell quantity, must be a positive integer, indicating how many shares to sell
signature: Model signature (optional, will use config/env if not provided)
today_date: Trading date (optional, will use config/env if not provided)
Returns:
Dict[str, Any]:
- Success: Returns new position dictionary (containing stock quantity and cash balance)
- Failure: Returns {"error": error message, ...} dictionary
Raises:
ValueError: Raised when SIGNATURE environment variable is not set
Example:
>>> result = sell("AAPL", 10)
>>> print(result) # {"AAPL": 90, "MSFT": 5, "CASH": 15000.0, ...}
"""
# Step 1: Get environment variables and basic information
# Get signature (model name) from environment variable, used to determine data storage path
signature = get_config_value("SIGNATURE")
# Get signature (model name) from parameter or fallback to config/env
if signature is None:
raise ValueError("SIGNATURE environment variable is not set")
# Get current trading date from environment variable
today_date = get_config_value("TODAY_DATE")
signature = get_config_value("SIGNATURE")
if signature is None:
raise ValueError("SIGNATURE not provided and environment variable is not set")
# Get current trading date from parameter or fallback to config/env
if today_date is None:
today_date = get_config_value("TODAY_DATE")
# Step 2: Get current latest position and operation ID
# get_latest_position returns two values: position dictionary and current maximum operation ID

View File

@@ -438,12 +438,19 @@ def _create_indexes(cursor: sqlite3.Cursor) -> None:
""")
# Positions table - add index for simulation_run_id and session_id
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_positions_run_id ON positions(simulation_run_id)
""")
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_positions_session_id ON positions(session_id)
""")
# Check if columns exist before creating indexes
cursor.execute("PRAGMA table_info(positions)")
position_columns = [row[1] for row in cursor.fetchall()]
if 'simulation_run_id' in position_columns:
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_positions_run_id ON positions(simulation_run_id)
""")
if 'session_id' in position_columns:
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_positions_session_id ON positions(session_id)
""")
def drop_all_tables(db_path: str = "data/jobs.db") -> None:

View File

@@ -126,7 +126,7 @@ class ModelDayExecutor:
os.environ["RUNTIME_ENV_PATH"] = self.runtime_config_path
# Initialize agent
agent = self._initialize_agent()
agent = await self._initialize_agent()
# Run trading session
logger.info(f"Running trading session for {self.model_sig} on {self.date}")
@@ -209,7 +209,7 @@ class ModelDayExecutor:
"""Execute model-day simulation (sync entry point)."""
return self.execute_sync()
def _initialize_agent(self):
async def _initialize_agent(self):
"""
Initialize trading agent with config.
@@ -259,6 +259,9 @@ class ModelDayExecutor:
# - Database initialization is handled by JobManager
# - File-based position tracking is only for standalone/CLI mode
# Initialize MCP client and AI model
await agent.initialize()
return agent
def _create_trading_session(self, cursor) -> int:

View File

@@ -7,7 +7,6 @@ services:
container_name: ai-trader-server
volumes:
- ${VOLUME_PATH:-.}/data:/app/data
- ${VOLUME_PATH:-.}/logs:/app/logs
# User configs mounted to /app/user-configs (default config baked into image)
- ${VOLUME_PATH:-.}/configs:/app/user-configs
environment:

View File

@@ -112,10 +112,9 @@ docker-compose up
### Volume Mounts
Docker Compose mounts three volumes for persistent data. By default, these are stored in the project directory:
Docker Compose mounts two volumes for persistent data. By default, these are stored in the project directory:
- `./data:/app/data` - Price data and trading records
- `./logs:/app/logs` - MCP service logs
- `./configs:/app/configs` - Configuration files (allows editing configs without rebuilding)
### Custom Volume Location
@@ -132,7 +131,6 @@ VOLUME_PATH=./volumes
This will store data in:
- `/home/user/trading-data/data/`
- `/home/user/trading-data/logs/`
- `/home/user/trading-data/configs/`
**Note:** The directory structure is automatically created. You'll need to copy your existing configs:
@@ -148,7 +146,7 @@ To reset all trading data:
```bash
docker-compose down
rm -rf ${VOLUME_PATH:-.}/data/agent_data/* ${VOLUME_PATH:-.}/logs/*
rm -rf ${VOLUME_PATH:-.}/data/agent_data/*
docker-compose up
```
@@ -175,8 +173,7 @@ docker pull ghcr.io/xe138/ai-trader-server:latest
```bash
docker run --env-file .env \
-v $(pwd)/data:/app/data \
-v $(pwd)/logs:/app/logs \
-p 8000-8003:8000-8003 \
-p 8080:8080 \
ghcr.io/xe138/ai-trader-server:latest
```
@@ -192,9 +189,9 @@ docker pull ghcr.io/xe138/ai-trader-server:v1.0.0
**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`
- Check if API port 8080 is already in use: `lsof -i :8080`
- Verify MCP services started by checking Docker logs for service startup messages
### Missing API Keys
@@ -216,12 +213,12 @@ docker pull ghcr.io/xe138/ai-trader-server:v1.0.0
### Permission Issues
**Symptom:** Cannot write to data or logs directories
**Symptom:** Cannot write to data directory
**Solutions:**
- Ensure directories writable: `chmod -R 755 data logs`
- Ensure data directory is writable: `chmod -R 755 data`
- Check volume mount permissions
- May need to create directories first: `mkdir -p data logs`
- May need to create directory first: `mkdir -p data`
### Container Keeps Restarting
@@ -256,13 +253,12 @@ docker buildx build --platform linux/amd64,linux/arm64 -t ai-trader-server .
docker stats ai-trader-server
```
### Access MCP Services Directly
### Access API Directly
Services exposed on host:
- Math: http://localhost:8000
- Search: http://localhost:8001
- Trade: http://localhost:8002
- Price: http://localhost:8003
API server exposed on host:
- REST API: http://localhost:8080
MCP services run internally and are not exposed to the host.
## Development Workflow