mirror of
https://github.com/Xe138/AI-Trader.git
synced 2026-04-01 17:17:24 -04:00
test: add integration tests for duplicate prevention and cross-job continuity
- Test duplicate simulation detection and skipping - Test portfolio continuity across multiple jobs - Verify warnings are returned for skipped simulations - Use database mocking for isolated test environments
This commit is contained in:
278
tests/integration/test_duplicate_simulation_prevention.py
Normal file
278
tests/integration/test_duplicate_simulation_prevention.py
Normal file
@@ -0,0 +1,278 @@
|
||||
"""Integration test for duplicate simulation prevention."""
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from api.job_manager import JobManager
|
||||
from api.model_day_executor import ModelDayExecutor
|
||||
from api.database import get_db_connection
|
||||
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_env(tmp_path):
|
||||
"""Create temporary environment with db and config."""
|
||||
# Create temp database
|
||||
db_path = str(tmp_path / "test_jobs.db")
|
||||
|
||||
# Initialize database
|
||||
conn = get_db_connection(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create schema
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
job_id TEXT PRIMARY KEY,
|
||||
config_path TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
date_range TEXT NOT NULL,
|
||||
models TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
started_at TEXT,
|
||||
updated_at TEXT,
|
||||
completed_at TEXT,
|
||||
total_duration_seconds REAL,
|
||||
error TEXT,
|
||||
warnings TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
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,
|
||||
started_at TEXT,
|
||||
completed_at TEXT,
|
||||
duration_seconds REAL,
|
||||
error TEXT,
|
||||
FOREIGN KEY (job_id) REFERENCES jobs(job_id) ON DELETE CASCADE,
|
||||
UNIQUE(job_id, date, model)
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS trading_days (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_id TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
starting_cash REAL NOT NULL,
|
||||
ending_cash REAL NOT NULL,
|
||||
profit REAL NOT NULL,
|
||||
return_pct REAL NOT NULL,
|
||||
portfolio_value REAL NOT NULL,
|
||||
reasoning_summary TEXT,
|
||||
reasoning_full TEXT,
|
||||
completed_at TEXT,
|
||||
session_duration_seconds REAL,
|
||||
UNIQUE(job_id, model, date)
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS holdings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trading_day_id INTEGER NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
quantity INTEGER NOT NULL,
|
||||
FOREIGN KEY (trading_day_id) REFERENCES trading_days(id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS actions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trading_day_id INTEGER NOT NULL,
|
||||
action_type TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
quantity INTEGER NOT NULL,
|
||||
price REAL NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (trading_day_id) REFERENCES trading_days(id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Create mock config
|
||||
config_path = str(tmp_path / "test_config.json")
|
||||
config = {
|
||||
"models": [
|
||||
{
|
||||
"signature": "test-model",
|
||||
"basemodel": "mock/model",
|
||||
"enabled": True
|
||||
}
|
||||
],
|
||||
"agent_config": {
|
||||
"max_steps": 10,
|
||||
"initial_cash": 10000.0
|
||||
},
|
||||
"log_config": {
|
||||
"log_path": str(tmp_path / "logs")
|
||||
},
|
||||
"date_range": {
|
||||
"init_date": "2025-10-13"
|
||||
}
|
||||
}
|
||||
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(config, f)
|
||||
|
||||
yield {
|
||||
"db_path": db_path,
|
||||
"config_path": config_path,
|
||||
"data_dir": str(tmp_path)
|
||||
}
|
||||
|
||||
|
||||
def test_duplicate_simulation_is_skipped(temp_env):
|
||||
"""Test that overlapping job skips already-completed simulation."""
|
||||
manager = JobManager(db_path=temp_env["db_path"])
|
||||
|
||||
# Create first job
|
||||
result_1 = manager.create_job(
|
||||
config_path=temp_env["config_path"],
|
||||
date_range=["2025-10-15"],
|
||||
models=["test-model"]
|
||||
)
|
||||
job_id_1 = result_1["job_id"]
|
||||
|
||||
# Simulate completion by manually inserting trading_day record
|
||||
conn = get_db_connection(temp_env["db_path"])
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO trading_days (
|
||||
job_id, model, date, starting_cash, ending_cash,
|
||||
profit, return_pct, portfolio_value, completed_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
job_id_1,
|
||||
"test-model",
|
||||
"2025-10-15",
|
||||
10000.0,
|
||||
9500.0,
|
||||
-500.0,
|
||||
-5.0,
|
||||
9500.0,
|
||||
"2025-11-07T01:00:00Z"
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Mark job_detail as completed
|
||||
manager.update_job_detail_status(
|
||||
job_id_1,
|
||||
"2025-10-15",
|
||||
"test-model",
|
||||
"completed"
|
||||
)
|
||||
|
||||
# Try to create second job with same model-day
|
||||
result_2 = manager.create_job(
|
||||
config_path=temp_env["config_path"],
|
||||
date_range=["2025-10-15", "2025-10-16"],
|
||||
models=["test-model"]
|
||||
)
|
||||
|
||||
# Should have warnings about skipped simulation
|
||||
assert len(result_2["warnings"]) == 1
|
||||
assert "2025-10-15" in result_2["warnings"][0]
|
||||
|
||||
# Should only create job_detail for 2025-10-16
|
||||
details = manager.get_job_details(result_2["job_id"])
|
||||
assert len(details) == 1
|
||||
assert details[0]["date"] == "2025-10-16"
|
||||
|
||||
|
||||
def test_portfolio_continues_from_previous_job(temp_env):
|
||||
"""Test that new job continues portfolio from previous job's last day."""
|
||||
manager = JobManager(db_path=temp_env["db_path"])
|
||||
|
||||
# Create and complete first job
|
||||
result_1 = manager.create_job(
|
||||
config_path=temp_env["config_path"],
|
||||
date_range=["2025-10-13"],
|
||||
models=["test-model"]
|
||||
)
|
||||
job_id_1 = result_1["job_id"]
|
||||
|
||||
# Insert completed trading_day with holdings
|
||||
conn = get_db_connection(temp_env["db_path"])
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO trading_days (
|
||||
job_id, model, date, starting_cash, ending_cash,
|
||||
profit, return_pct, portfolio_value, completed_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
job_id_1,
|
||||
"test-model",
|
||||
"2025-10-13",
|
||||
10000.0,
|
||||
5000.0,
|
||||
0.0,
|
||||
0.0,
|
||||
15000.0,
|
||||
"2025-11-07T01:00:00Z"
|
||||
))
|
||||
|
||||
trading_day_id = cursor.lastrowid
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO holdings (trading_day_id, symbol, quantity)
|
||||
VALUES (?, ?, ?)
|
||||
""", (trading_day_id, "AAPL", 10))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Mark as completed
|
||||
manager.update_job_detail_status(job_id_1, "2025-10-13", "test-model", "completed")
|
||||
manager.update_job_status(job_id_1, "completed")
|
||||
|
||||
# Create second job for next day
|
||||
result_2 = manager.create_job(
|
||||
config_path=temp_env["config_path"],
|
||||
date_range=["2025-10-14"],
|
||||
models=["test-model"]
|
||||
)
|
||||
job_id_2 = result_2["job_id"]
|
||||
|
||||
# Get starting position for 2025-10-14
|
||||
from agent_tools.tool_trade import get_current_position_from_db
|
||||
import agent_tools.tool_trade as trade_module
|
||||
original_get_db_connection = trade_module.get_db_connection
|
||||
|
||||
def mock_get_db_connection(path):
|
||||
return get_db_connection(temp_env["db_path"])
|
||||
|
||||
trade_module.get_db_connection = mock_get_db_connection
|
||||
|
||||
try:
|
||||
position, _ = get_current_position_from_db(
|
||||
job_id=job_id_2,
|
||||
model="test-model",
|
||||
date="2025-10-14",
|
||||
initial_cash=10000.0
|
||||
)
|
||||
|
||||
# Should continue from job 1's ending position
|
||||
assert position["CASH"] == 5000.0
|
||||
assert position["AAPL"] == 10
|
||||
finally:
|
||||
# Restore original function
|
||||
trade_module.get_db_connection = original_get_db_connection
|
||||
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user