Compare commits

..

5 Commits

Author SHA1 Message Date
1c19eea29a debug: add comprehensive diagnostic logging for ContextInjector flow
Add instrumentation at component boundaries to trace where ContextInjector values become None:
- ModelDayExecutor: Log ContextInjector creation and set_context() invocation
- BaseAgent.set_context(): Log entry, client creation, tool reload, completion
- Includes object IDs to verify instance identity across boundaries

Part of systematic debugging investigation for issue #TBD.
2025-11-02 23:05:40 -05:00
e968434062 fix: reload tools after context injection and prevent database locking
Critical fixes for ContextInjector and database concurrency:

1. ContextInjector Not Working:
   - Made set_context() async to reload tools after recreating MCP client
   - Tools from old client (without interceptor) were still being used
   - Now tools are reloaded from new client with interceptor active
   - This ensures buy/sell calls properly receive injected parameters

2. Database Locking:
   - Closed main connection before _write_results_to_db() opens new one
   - SQLite doesn't handle concurrent write connections well
   - Prevents "database is locked" error during position writes

Changes:
- agent/base_agent/base_agent.py:
  - async def set_context() instead of def set_context()
  - Added: self.tools = await self.client.get_tools()
- api/model_day_executor.py:
  - await agent.set_context(context_injector)
  - conn.close() before _write_results_to_db()

Root Cause:
When recreating the MCP client with tool_interceptors, the old tools
were still cached in self.tools and being passed to the AI agent.
The interceptor was never invoked, so job_id/signature/date were missing.
2025-11-02 22:42:17 -05:00
4c1d23a7c8 fix: correct get_db_path() usage to pass base database path
The get_db_path() function requires a base_db_path argument
to properly resolve PROD vs DEV database paths. Updated all
calls to pass "data/jobs.db" as the base path.

Changes:
- agent_tools/tool_trade.py: Fix 3 occurrences (lines 33, 113, 236)
- tools/price_tools.py: Fix 2 occurrences in new database functions
- Remove unused get_db_path import from tool_trade.py

This fixes TypeError when running simulations:
  get_db_path() missing 1 required positional argument: 'base_db_path'

The get_db_connection() function internally calls get_db_path()
to resolve the correct database path based on DEPLOYMENT_MODE.
2025-11-02 22:26:45 -05:00
027b4bd8e4 refactor: implement database-only position tracking with lazy context injection
This commit migrates the system to database-only position storage,
eliminating file-based position.jsonl dependencies and fixing
ContextInjector initialization timing issues.

Key Changes:

1. ContextInjector Lifecycle Refactor:
   - Remove ContextInjector creation from BaseAgent.__init__()
   - Add BaseAgent.set_context() method for post-initialization injection
   - Update ModelDayExecutor to create ContextInjector with correct trading day date
   - Ensures ContextInjector receives actual trading date instead of init_date
   - Includes session_id injection for proper database linking

2. Database Position Functions:
   - Implement get_today_init_position_from_db() for querying previous positions
   - Implement add_no_trade_record_to_db() for no-trade day handling
   - Both functions query SQLite directly (positions + holdings tables)
   - Handle first trading day case with initial cash return
   - Include comprehensive error handling and logging

3. System Integration:
   - Update get_agent_system_prompt() to use database queries
   - Update _handle_trading_result() to write no-trade records to database
   - Remove dependencies on position.jsonl file reading/writing
   - Use deployment_config for automatic prod/dev database resolution

Data Flow:
- ModelDayExecutor creates runtime config and trading session
- Agent initialized without context
- ContextInjector created with (signature, date, job_id, session_id)
- Context injected via set_context()
- System prompt queries database for yesterday's position
- Trade tools write directly to database
- No-trade handler creates database records

Fixes:
- ContextInjector no longer receives None values
- No FileNotFoundError for missing position.jsonl files
- Database is single source of truth for position tracking
- Session linking maintained across all position records

Design: docs/plans/2025-02-11-database-position-tracking-design.md
2025-11-02 22:20:01 -05:00
7a734d265b docs: add database-only position tracking design
Created comprehensive design document addressing:
- ContextInjector initialization timing issues
- Migration from file-based to database-only position tracking
- Complete data flow and integration strategy
- Testing and validation approach

Design resolves two critical simulation failures:
1. ContextInjector receiving None values for trade tool parameters
2. FileNotFoundError when accessing position.jsonl files

Ready for implementation.
2025-11-02 22:11:00 -05:00
6 changed files with 735 additions and 33 deletions

View File

@@ -173,21 +173,13 @@ class BaseAgent:
print("⚠️ OpenAI base URL not set, using default")
try:
# Get job_id from runtime config if available (API mode)
from tools.general_tools import get_config_value
job_id = get_config_value("JOB_ID") # Returns None if not in API mode
# Context injector will be set later via set_context() method
self.context_injector = None
# 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
job_id=job_id # Will be None in standalone mode, populated in API mode
)
# Create MCP client with interceptor
# Create MCP client without interceptors initially
self.client = MultiServerMCPClient(
self.mcp_config,
tool_interceptors=[self.context_injector]
tool_interceptors=[]
)
# Get tools
@@ -229,6 +221,40 @@ class BaseAgent:
print(f"✅ Agent {self.signature} initialization completed")
async def set_context(self, context_injector: "ContextInjector") -> None:
"""
Inject ContextInjector after initialization.
This allows the ContextInjector to be created with the correct
trading day date and session_id after the agent is initialized.
Args:
context_injector: Configured ContextInjector instance with
correct signature, today_date, job_id, session_id
"""
print(f"[DEBUG] set_context() ENTRY: Received context_injector with signature={context_injector.signature}, date={context_injector.today_date}, job_id={context_injector.job_id}, session_id={context_injector.session_id}")
self.context_injector = context_injector
print(f"[DEBUG] set_context(): Set self.context_injector, id={id(self.context_injector)}")
# Recreate MCP client with the interceptor
# Note: We need to recreate because MultiServerMCPClient doesn't have add_interceptor()
print(f"[DEBUG] set_context(): Creating new MCP client with interceptor, id={id(context_injector)}")
self.client = MultiServerMCPClient(
self.mcp_config,
tool_interceptors=[context_injector]
)
print(f"[DEBUG] set_context(): MCP client created")
# CRITICAL: Reload tools from new client so they use the interceptor
print(f"[DEBUG] set_context(): Reloading tools...")
self.tools = await self.client.get_tools()
print(f"[DEBUG] set_context(): Tools reloaded, count={len(self.tools)}")
print(f"✅ Context injected: signature={context_injector.signature}, "
f"date={context_injector.today_date}, job_id={context_injector.job_id}, "
f"session_id={context_injector.session_id}")
def _capture_message(self, role: str, content: str, tool_name: str = None, tool_input: str = None) -> None:
"""
Capture a message in conversation history.
@@ -429,18 +455,32 @@ Summary:"""
await self._handle_trading_result(today_date)
async def _handle_trading_result(self, today_date: str) -> None:
"""Handle trading results"""
"""Handle trading results with database writes."""
from tools.price_tools import add_no_trade_record_to_db
if_trade = get_config_value("IF_TRADE")
if if_trade:
write_config_value("IF_TRADE", False)
print("✅ Trading completed")
else:
print("📊 No trading, maintaining positions")
try:
add_no_trade_record(today_date, self.signature)
except NameError as e:
print(f"❌ NameError: {e}")
raise
# Get context from runtime config
job_id = get_config_value("JOB_ID")
session_id = self.context_injector.session_id if self.context_injector else None
if not job_id or not session_id:
raise ValueError("Missing JOB_ID or session_id for no-trade record")
# Write no-trade record to database
add_no_trade_record_to_db(
today_date,
self.signature,
job_id,
session_id
)
write_config_value("IF_TRADE", False)
def register_agent(self) -> None:

View File

@@ -7,7 +7,6 @@ project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, project_root)
from tools.price_tools import get_open_prices
import json
from tools.deployment_config import get_db_path
from api.database import get_db_connection
from datetime import datetime
mcp = FastMCP("TradeTools")
@@ -30,7 +29,7 @@ def get_current_position_from_db(job_id: str, model: str, date: str) -> Tuple[Di
Raises:
Exception: If database query fails
"""
db_path = get_db_path()
db_path = "data/jobs.db"
conn = get_db_connection(db_path)
cursor = conn.cursor()
@@ -110,7 +109,7 @@ def buy(symbol: str, amount: int, signature: str = None, today_date: str = None,
if not today_date:
return {"error": "Missing required parameter: today_date"}
db_path = get_db_path()
db_path = "data/jobs.db"
conn = get_db_connection(db_path)
cursor = conn.cursor()
@@ -233,7 +232,7 @@ def sell(symbol: str, amount: int, signature: str = None, today_date: str = None
if not today_date:
return {"error": "Missing required parameter: today_date"}
db_path = get_db_path()
db_path = "data/jobs.db"
conn = get_db_connection(db_path)
cursor = conn.cursor()

View File

@@ -129,12 +129,21 @@ class ModelDayExecutor:
# Set environment variable for agent to use isolated config
os.environ["RUNTIME_ENV_PATH"] = self.runtime_config_path
# Initialize agent
# Initialize agent (without context)
agent = await self._initialize_agent()
# Update context injector with session_id
if hasattr(agent, 'context_injector') and agent.context_injector:
agent.context_injector.session_id = session_id
# Create and inject context with correct values
from agent.context_injector import ContextInjector
context_injector = ContextInjector(
signature=self.model_sig,
today_date=self.date, # Current trading day
job_id=self.job_id,
session_id=session_id
)
logger.info(f"[DEBUG] ModelDayExecutor: Created ContextInjector with signature={self.model_sig}, date={self.date}, job_id={self.job_id}, session_id={session_id}")
logger.info(f"[DEBUG] ModelDayExecutor: Calling await agent.set_context()")
await agent.set_context(context_injector)
logger.info(f"[DEBUG] ModelDayExecutor: set_context() completed")
# Run trading session
logger.info(f"Running trading session for {self.model_sig} on {self.date}")
@@ -149,10 +158,13 @@ class ModelDayExecutor:
# Update session summary
await self._update_session_summary(cursor, session_id, conversation, agent)
# Store positions (pass session_id)
self._write_results_to_db(agent, session_id)
# Commit and close connection before _write_results_to_db opens a new one
conn.commit()
conn.close()
conn = None # Mark as closed
# Store positions (pass session_id) - this opens its own connection
self._write_results_to_db(agent, session_id)
# Update status to completed
self.job_manager.update_job_detail_status(

View File

@@ -0,0 +1,476 @@
# Database-Only Position Tracking Design
**Date:** 2025-02-11
**Status:** Approved
**Version:** 1.0
## Problem Statement
Two critical issues prevent simulations from running:
1. **ContextInjector receives None values**: The ContextInjector shows `{'signature': None, 'today_date': None, 'job_id': None, 'session_id': None}` when injecting parameters into trade tool calls, causing trade validation to fail.
2. **File-based position tracking still in use**: System prompt builder and no-trade handler attempt to read/write position.jsonl files that no longer exist after SQLite migration.
## Root Cause Analysis
### Issue 1: ContextInjector Initialization Timing
**Problem Chain:**
- `BaseAgent.__init__()` creates `ContextInjector` with `self.init_date`
- `init_date` is the START of simulation date range (e.g., "2025-10-13"), not current trading day ("2025-10-01")
- Runtime config contains correct values (`TODAY_DATE="2025-10-01"`, `SIGNATURE="gpt-5"`, `JOB_ID="dc488e87..."`), but BaseAgent doesn't use them during initialization
- ContextInjector is created before the trading session, so it doesn't know the correct date
**Evidence:**
```
ai-trader-app | [ContextInjector] Tool: buy, Args after injection: {'symbol': 'MSFT', 'amount': 1, 'signature': None, 'today_date': None, 'job_id': None, 'session_id': None}
```
### Issue 2: Mixed Storage Architecture
**Problem Chain:**
- Trade tools (tool_trade.py) query/write to SQLite database
- System prompt builder calls `get_today_init_position()` which reads position.jsonl files
- No-trade handler calls `add_no_trade_record()` which writes to position.jsonl files
- Files don't exist because we migrated to database-only storage
**Evidence:**
```
FileNotFoundError: [Errno 2] No such file or directory: '/app/data/agent_data/gpt-5/position/position.jsonl'
```
## Design Solution
### Architecture Principles
1. **Database-only position storage**: All position queries and writes go through SQLite
2. **Lazy context injection**: Create ContextInjector after runtime config is written and session is created
3. **Real-time database queries**: System prompt builder queries database directly, no file caching
4. **Clean initialization order**: Config → Database → Agent → Context → Session
### Component Changes
#### 1. ContextInjector Lifecycle Refactor
**BaseAgent Changes:**
Remove ContextInjector creation from `__init__()`:
```python
# OLD (in __init__)
self.context_injector = ContextInjector(
signature=self.signature,
today_date=self.init_date, # WRONG: uses start date
job_id=job_id
)
self.client = MultiServerMCPClient(
self.mcp_config,
tool_interceptors=[self.context_injector]
)
# NEW (in __init__)
self.context_injector = None
self.client = MultiServerMCPClient(
self.mcp_config,
tool_interceptors=[] # Empty initially
)
```
Add new method `set_context()`:
```python
def set_context(self, context_injector: ContextInjector) -> None:
"""Inject ContextInjector after initialization.
Args:
context_injector: Configured ContextInjector instance
"""
self.context_injector = context_injector
self.client.add_interceptor(context_injector)
```
**ModelDayExecutor Changes:**
Create and inject ContextInjector after agent initialization:
```python
async def execute_async(self) -> Dict[str, Any]:
# ... create session, initialize position ...
# Set RUNTIME_ENV_PATH
os.environ["RUNTIME_ENV_PATH"] = self.runtime_config_path
# Initialize agent (without context)
agent = await self._initialize_agent()
# Create context injector with correct values
context_injector = ContextInjector(
signature=self.model_sig,
today_date=self.date, # CORRECT: current trading day
job_id=self.job_id,
session_id=session_id
)
# Inject context into agent
agent.set_context(context_injector)
# Run trading session
session_result = await agent.run_trading_session(self.date)
```
#### 2. Database Position Query Functions
**New Functions (tools/price_tools.py):**
```python
def get_today_init_position_from_db(
today_date: str,
modelname: str,
job_id: str
) -> Dict[str, float]:
"""
Query yesterday's position from database.
Args:
today_date: Current trading date (YYYY-MM-DD)
modelname: Model signature
job_id: Job UUID
Returns:
Position dict: {"AAPL": 50, "MSFT": 30, "CASH": 5000.0}
If no position exists: {"CASH": 10000.0} (initial cash)
"""
from tools.deployment_config import get_db_path
from api.database import get_db_connection
db_path = get_db_path()
conn = get_db_connection(db_path)
cursor = conn.cursor()
try:
# Get most recent position before today
cursor.execute("""
SELECT p.id, p.cash
FROM positions p
WHERE p.job_id = ? AND p.model = ? AND p.date < ?
ORDER BY p.date DESC, p.action_id DESC
LIMIT 1
""", (job_id, modelname, today_date))
row = cursor.fetchone()
if not row:
# First day - return initial cash
return {"CASH": 10000.0} # TODO: Read from config
position_id, cash = row
position_dict = {"CASH": cash}
# Get holdings for this position
cursor.execute("""
SELECT symbol, quantity
FROM holdings
WHERE position_id = ?
""", (position_id,))
for symbol, quantity in cursor.fetchall():
position_dict[symbol] = quantity
return position_dict
finally:
conn.close()
def add_no_trade_record_to_db(
today_date: str,
modelname: str,
job_id: str,
session_id: int
) -> None:
"""
Create no-trade position record in database.
Args:
today_date: Current trading date (YYYY-MM-DD)
modelname: Model signature
job_id: Job UUID
session_id: Trading session ID
"""
from tools.deployment_config import get_db_path
from api.database import get_db_connection
from agent_tools.tool_trade import get_current_position_from_db
from datetime import datetime
db_path = get_db_path()
conn = get_db_connection(db_path)
cursor = conn.cursor()
try:
# Get current position
current_position, next_action_id = get_current_position_from_db(
job_id, modelname, today_date
)
# Calculate portfolio value
# (Reuse logic from tool_trade.py)
cash = current_position.get("CASH", 0.0)
portfolio_value = cash
# Add stock values
for symbol, qty in current_position.items():
if symbol != "CASH":
try:
from tools.price_tools import get_open_prices
price = get_open_prices(today_date, [symbol])[f'{symbol}_price']
portfolio_value += qty * price
except KeyError:
pass
# Get previous value for P&L
cursor.execute("""
SELECT portfolio_value
FROM positions
WHERE job_id = ? AND model = ? AND date < ?
ORDER BY date DESC, action_id DESC
LIMIT 1
""", (job_id, modelname, today_date))
row = cursor.fetchone()
previous_value = row[0] if row else 10000.0
daily_profit = portfolio_value - previous_value
daily_return_pct = (daily_profit / previous_value * 100) if previous_value > 0 else 0
# Insert position record
created_at = datetime.utcnow().isoformat() + "Z"
cursor.execute("""
INSERT INTO positions (
job_id, date, model, action_id, action_type,
cash, portfolio_value, daily_profit, daily_return_pct,
session_id, created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
job_id, today_date, modelname, next_action_id, "no_trade",
cash, portfolio_value, daily_profit, daily_return_pct,
session_id, created_at
))
position_id = cursor.lastrowid
# Insert holdings (unchanged from previous position)
for symbol, qty in current_position.items():
if symbol != "CASH":
cursor.execute("""
INSERT INTO holdings (position_id, symbol, quantity)
VALUES (?, ?, ?)
""", (position_id, symbol, qty))
conn.commit()
except Exception as e:
conn.rollback()
raise
finally:
conn.close()
```
#### 3. System Prompt Builder Updates
**Modified Function (prompts/agent_prompt.py):**
```python
def get_agent_system_prompt(today_date: str, signature: str) -> str:
"""Build system prompt with database position queries."""
from tools.general_tools import get_config_value
print(f"signature: {signature}")
print(f"today_date: {today_date}")
# Get job_id from runtime config
job_id = get_config_value("JOB_ID")
if not job_id:
raise ValueError("JOB_ID not found in runtime config")
# Query database for yesterday's position
today_init_position = get_today_init_position_from_db(
today_date, signature, job_id
)
# Get prices (unchanged)
yesterday_buy_prices, yesterday_sell_prices = get_yesterday_open_and_close_price(
today_date, all_nasdaq_100_symbols
)
today_buy_price = get_open_prices(today_date, all_nasdaq_100_symbols)
yesterday_profit = get_yesterday_profit(
today_date, yesterday_buy_prices, yesterday_sell_prices, today_init_position
)
return agent_system_prompt.format(
date=today_date,
positions=today_init_position,
STOP_SIGNAL=STOP_SIGNAL,
yesterday_close_price=yesterday_sell_prices,
today_buy_price=today_buy_price,
yesterday_profit=yesterday_profit
)
```
#### 4. No-Trade Handler Updates
**Modified Method (agent/base_agent/base_agent.py):**
```python
async def _handle_trading_result(self, today_date: str) -> None:
"""Handle trading results with database writes."""
from tools.general_tools import get_config_value
from tools.price_tools import add_no_trade_record_to_db
if_trade = get_config_value("IF_TRADE")
if if_trade:
write_config_value("IF_TRADE", False)
print("✅ Trading completed")
else:
print("📊 No trading, maintaining positions")
# Get context from runtime config
job_id = get_config_value("JOB_ID")
session_id = self.context_injector.session_id if self.context_injector else None
if not job_id or not session_id:
raise ValueError("Missing JOB_ID or session_id for no-trade record")
# Write no-trade record to database
add_no_trade_record_to_db(
today_date,
self.signature,
job_id,
session_id
)
write_config_value("IF_TRADE", False)
```
### Data Flow Summary
**Complete Execution Sequence:**
1. `ModelDayExecutor.__init__()`:
- Create runtime config file with TODAY_DATE, SIGNATURE, JOB_ID
2. `ModelDayExecutor.execute_async()`:
- Create trading_sessions record → get session_id
- Initialize starting position (if first day)
- Set RUNTIME_ENV_PATH environment variable
- Initialize agent (without ContextInjector)
- Create ContextInjector(date, model_sig, job_id, session_id)
- Call agent.set_context(context_injector)
- Run trading session
3. `BaseAgent.run_trading_session()`:
- Build system prompt → queries database for yesterday's position
- AI agent analyzes and decides
- Calls buy/sell tools → ContextInjector injects parameters
- Trade tools write to database
- If no trade: add_no_trade_record_to_db()
4. Position Query Flow:
- System prompt needs yesterday's position
- `get_today_init_position_from_db(today_date, signature, job_id)`
- Query: `SELECT positions + holdings WHERE job_id=? AND model=? AND date<? ORDER BY date DESC, action_id DESC LIMIT 1`
- Reconstruct position dict from results
- Return to system prompt builder
### Testing Strategy
**Critical Test Cases:**
1. **First Trading Day**
- No previous position in database
- Returns `{"CASH": 10000.0}`
- System prompt shows available cash
- Initial position created with action_id=0
2. **Subsequent Trading Days**
- Query finds previous position
- System prompt shows yesterday's holdings
- Action_id increments properly
3. **No-Trade Days**
- Agent outputs `<FINISH_SIGNAL>` without trading
- `add_no_trade_record_to_db()` creates record
- Holdings unchanged
- Portfolio value calculated
4. **ContextInjector Values**
- All parameters non-None
- Debug log shows correct injection
- Trade tools validate successfully
**Edge Cases:**
- Multiple models, same job (different signatures)
- Date gaps (weekends) - query finds Friday on Monday
- Mid-simulation restart - resumes from last position
- Empty holdings (only CASH)
**Validation Points:**
- Log ContextInjector values at injection
- Log database query results
- Verify initial position created
- Check session_id links positions
## Implementation Checklist
### Phase 1: ContextInjector Refactor
- [ ] Remove ContextInjector creation from BaseAgent.__init__()
- [ ] Add BaseAgent.set_context() method
- [ ] Update ModelDayExecutor to create and inject ContextInjector
- [ ] Add debug logging for injected values
### Phase 2: Database Position Functions
- [ ] Implement get_today_init_position_from_db()
- [ ] Implement add_no_trade_record_to_db()
- [ ] Add database error handling
- [ ] Add logging for query results
### Phase 3: Integration
- [ ] Update get_agent_system_prompt() to use database queries
- [ ] Update _handle_trading_result() to use database writes
- [ ] Remove/deprecate old file-based functions
- [ ] Test first trading day scenario
- [ ] Test subsequent trading days
- [ ] Test no-trade scenario
### Phase 4: Validation
- [ ] Run full simulation and verify ContextInjector logs
- [ ] Verify initial cash appears in system prompt
- [ ] Verify trades execute successfully
- [ ] Verify no-trade records created
- [ ] Check database for correct position records
## Rollback Plan
If issues arise:
1. Revert ContextInjector changes (keep in __init__)
2. Temporarily pass correct date via environment variable
3. Keep file-based functions as fallback
4. Debug database queries in isolation
## Success Criteria
1. ContextInjector logs show all non-None values
2. System prompt displays initial $10,000 cash
3. Trade tools successfully execute buy/sell operations
4. No FileNotFoundError exceptions
5. Database contains correct position records
6. AI agent can complete full trading day
## Notes
- File-based functions marked as deprecated but not removed (backward compatibility)
- Database queries use deployment_config for automatic prod/dev resolution
- Initial cash value should eventually be read from config, not hardcoded
- Consider adding database connection pooling for performance

View File

@@ -68,14 +68,24 @@ When you think your task is complete, output
def get_agent_system_prompt(today_date: str, signature: str) -> str:
print(f"signature: {signature}")
print(f"today_date: {today_date}")
# Get job_id from runtime config
job_id = get_config_value("JOB_ID")
if not job_id:
raise ValueError("JOB_ID not found in runtime config")
# Query database for yesterday's position
from tools.price_tools import get_today_init_position_from_db
today_init_position = get_today_init_position_from_db(today_date, signature, job_id)
# Get yesterday's buy and sell prices
yesterday_buy_prices, yesterday_sell_prices = get_yesterday_open_and_close_price(today_date, all_nasdaq_100_symbols)
today_buy_price = get_open_prices(today_date, all_nasdaq_100_symbols)
today_init_position = get_today_init_position(today_date, signature)
yesterday_profit = get_yesterday_profit(today_date, yesterday_buy_prices, yesterday_sell_prices, today_init_position)
return agent_system_prompt.format(
date=today_date,
positions=today_init_position,
date=today_date,
positions=today_init_position,
STOP_SIGNAL=STOP_SIGNAL,
yesterday_close_price=yesterday_sell_prices,
today_buy_price=today_buy_price,

View File

@@ -299,7 +299,172 @@ def add_no_trade_record(today_date: str, modelname: str):
with position_file.open("a", encoding="utf-8") as f:
f.write(json.dumps(save_item) + "\n")
return
return
def get_today_init_position_from_db(
today_date: str,
modelname: str,
job_id: str
) -> Dict[str, float]:
"""
Query yesterday's position from SQLite database.
Args:
today_date: Current trading date (YYYY-MM-DD)
modelname: Model signature
job_id: Job UUID
Returns:
Position dict: {"AAPL": 50, "MSFT": 30, "CASH": 5000.0}
If no position exists: {"CASH": 10000.0} (initial cash)
"""
import logging
from api.database import get_db_connection
logger = logging.getLogger(__name__)
db_path = "data/jobs.db"
conn = get_db_connection(db_path)
cursor = conn.cursor()
try:
# Get most recent position before today
cursor.execute("""
SELECT p.id, p.cash
FROM positions p
WHERE p.job_id = ? AND p.model = ? AND p.date < ?
ORDER BY p.date DESC, p.action_id DESC
LIMIT 1
""", (job_id, modelname, today_date))
row = cursor.fetchone()
if not row:
# First day - return initial cash
logger.info(f"No previous position found for {modelname}, returning initial cash")
return {"CASH": 10000.0}
position_id, cash = row
position_dict = {"CASH": cash}
# Get holdings for this position
cursor.execute("""
SELECT symbol, quantity
FROM holdings
WHERE position_id = ?
""", (position_id,))
for symbol, quantity in cursor.fetchall():
position_dict[symbol] = quantity
logger.debug(f"Loaded position for {modelname}: {position_dict}")
return position_dict
except Exception as e:
logger.error(f"Database error in get_today_init_position_from_db: {e}")
raise
finally:
conn.close()
def add_no_trade_record_to_db(
today_date: str,
modelname: str,
job_id: str,
session_id: int
) -> None:
"""
Create no-trade position record in SQLite database.
Args:
today_date: Current trading date (YYYY-MM-DD)
modelname: Model signature
job_id: Job UUID
session_id: Trading session ID
"""
import logging
from api.database import get_db_connection
from agent_tools.tool_trade import get_current_position_from_db
from datetime import datetime
logger = logging.getLogger(__name__)
db_path = "data/jobs.db"
conn = get_db_connection(db_path)
cursor = conn.cursor()
try:
# Get current position
current_position, next_action_id = get_current_position_from_db(
job_id, modelname, today_date
)
# Calculate portfolio value
cash = current_position.get("CASH", 0.0)
portfolio_value = cash
# Add stock values
for symbol, qty in current_position.items():
if symbol != "CASH":
try:
price = get_open_prices(today_date, [symbol])[f'{symbol}_price']
portfolio_value += qty * price
except KeyError:
logger.warning(f"Price not found for {symbol} on {today_date}")
pass
# Get previous value for P&L
cursor.execute("""
SELECT portfolio_value
FROM positions
WHERE job_id = ? AND model = ? AND date < ?
ORDER BY date DESC, action_id DESC
LIMIT 1
""", (job_id, modelname, today_date))
row = cursor.fetchone()
previous_value = row[0] if row else 10000.0
daily_profit = portfolio_value - previous_value
daily_return_pct = (daily_profit / previous_value * 100) if previous_value > 0 else 0
# Insert position record
created_at = datetime.utcnow().isoformat() + "Z"
cursor.execute("""
INSERT INTO positions (
job_id, date, model, action_id, action_type,
cash, portfolio_value, daily_profit, daily_return_pct,
session_id, created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
job_id, today_date, modelname, next_action_id, "no_trade",
cash, portfolio_value, daily_profit, daily_return_pct,
session_id, created_at
))
position_id = cursor.lastrowid
# Insert holdings (unchanged from previous position)
for symbol, qty in current_position.items():
if symbol != "CASH":
cursor.execute("""
INSERT INTO holdings (position_id, symbol, quantity)
VALUES (?, ?, ?)
""", (position_id, symbol, qty))
conn.commit()
logger.info(f"Created no-trade record for {modelname} on {today_date}")
except Exception as e:
conn.rollback()
logger.error(f"Database error in add_no_trade_record_to_db: {e}")
raise
finally:
conn.close()
if __name__ == "__main__":
today_date = get_config_value("TODAY_DATE")