mirror of
https://github.com/Xe138/AI-Trader.git
synced 2026-04-01 17:17:24 -04:00
585 lines
15 KiB
Markdown
585 lines
15 KiB
Markdown
# Daily P&L Calculation & Results API Refactor - Design Document
|
||
|
||
**Date:** 2025-11-03
|
||
**Status:** Approved - Ready for Implementation
|
||
|
||
---
|
||
|
||
## Problem Statement
|
||
|
||
The current results API returns data in an action-centric format where every trade action is a separate record. This has several issues:
|
||
|
||
1. **Incorrect Daily Metrics:** `daily_profit` and `daily_return_pct` always return 0
|
||
2. **Data Structure:** Multiple position records per day with redundant portfolio snapshots
|
||
3. **API Design:** Separate `/results` and `/reasoning` endpoints that should be unified
|
||
4. **Missing Context:** No clear distinction between starting/ending positions for a day
|
||
|
||
**Example of Current Incorrect Output:**
|
||
```json
|
||
{
|
||
"daily_profit": 0,
|
||
"daily_return_pct": 0,
|
||
"portfolio_value": 10062.15
|
||
}
|
||
```
|
||
|
||
Even though portfolio clearly changed from $9,957.96 to $10,062.15.
|
||
|
||
---
|
||
|
||
## Solution Design
|
||
|
||
### Core Principles
|
||
|
||
1. **Day-Centric Data Model:** Each trading day is the primary unit, not individual actions
|
||
2. **Ledger-Based Holdings:** Use snapshot approach (ending holdings only) for performance
|
||
3. **Calculate P&L at Market Open:** Value yesterday's holdings at today's prices
|
||
4. **Unified API:** Single `/results` endpoint with optional reasoning parameter
|
||
5. **AI-Generated Summaries:** Create summaries during simulation, not on-demand
|
||
|
||
---
|
||
|
||
## Database Schema (Normalized)
|
||
|
||
### trading_days Table
|
||
|
||
**Purpose:** Core table for each model-day execution with daily metrics
|
||
|
||
```sql
|
||
CREATE TABLE trading_days (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
job_id TEXT NOT NULL,
|
||
model TEXT NOT NULL,
|
||
date TEXT NOT NULL,
|
||
|
||
-- Starting state (cash only, holdings from previous day)
|
||
starting_cash REAL NOT NULL,
|
||
starting_portfolio_value REAL NOT NULL,
|
||
|
||
-- Daily performance metrics
|
||
daily_profit REAL NOT NULL,
|
||
daily_return_pct REAL NOT NULL,
|
||
|
||
-- Ending state (cash only, holdings in separate table)
|
||
ending_cash REAL NOT NULL,
|
||
ending_portfolio_value REAL NOT NULL,
|
||
|
||
-- Reasoning
|
||
reasoning_summary TEXT,
|
||
reasoning_full TEXT, -- JSON array
|
||
|
||
-- Metadata
|
||
total_actions INTEGER DEFAULT 0,
|
||
session_duration_seconds REAL,
|
||
days_since_last_trading INTEGER DEFAULT 1,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
completed_at TIMESTAMP,
|
||
|
||
UNIQUE(job_id, model, date),
|
||
FOREIGN KEY (job_id) REFERENCES jobs(job_id)
|
||
);
|
||
|
||
CREATE INDEX idx_trading_days_lookup ON trading_days(job_id, model, date);
|
||
```
|
||
|
||
### holdings Table
|
||
|
||
**Purpose:** Ending portfolio snapshots (starting holdings derived from previous day)
|
||
|
||
```sql
|
||
CREATE TABLE 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,
|
||
UNIQUE(trading_day_id, symbol)
|
||
);
|
||
|
||
CREATE INDEX idx_holdings_day ON holdings(trading_day_id);
|
||
```
|
||
|
||
**Key Design Decision:** Only store ending holdings. Starting holdings = previous day's ending holdings.
|
||
|
||
### actions Table
|
||
|
||
**Purpose:** Trade execution ledger
|
||
|
||
```sql
|
||
CREATE TABLE actions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
trading_day_id INTEGER NOT NULL,
|
||
|
||
action_type TEXT NOT NULL, -- 'buy', 'sell', 'no_trade'
|
||
symbol TEXT,
|
||
quantity INTEGER,
|
||
price REAL,
|
||
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
||
FOREIGN KEY (trading_day_id) REFERENCES trading_days(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE INDEX idx_actions_day ON actions(trading_day_id);
|
||
```
|
||
|
||
---
|
||
|
||
## Daily P&L Calculation Logic
|
||
|
||
### When to Calculate
|
||
|
||
**Timing:** At the start of each trading day, after loading current market prices.
|
||
|
||
### Calculation Method
|
||
|
||
```python
|
||
def calculate_daily_pnl(previous_day, current_date, current_prices):
|
||
"""
|
||
Calculate P&L by valuing yesterday's holdings at today's prices.
|
||
|
||
Args:
|
||
previous_day: {
|
||
"date": "2025-01-15",
|
||
"ending_cash": 9000.0,
|
||
"ending_portfolio_value": 10000.0,
|
||
"holdings": [{"symbol": "AAPL", "quantity": 10}]
|
||
}
|
||
current_date: "2025-01-16"
|
||
current_prices: {"AAPL": 150.0}
|
||
|
||
Returns:
|
||
{
|
||
"daily_profit": 500.0,
|
||
"daily_return_pct": 5.0,
|
||
"starting_portfolio_value": 10500.0,
|
||
"days_since_last_trading": 1
|
||
}
|
||
"""
|
||
if previous_day is None:
|
||
# First trading day
|
||
return {
|
||
"daily_profit": 0.0,
|
||
"daily_return_pct": 0.0,
|
||
"starting_portfolio_value": initial_cash,
|
||
"days_since_last_trading": 0
|
||
}
|
||
|
||
# Value previous holdings at current prices
|
||
current_value = cash
|
||
for holding in previous_holdings:
|
||
current_value += holding["quantity"] * current_prices[holding["symbol"]]
|
||
|
||
# Calculate P&L
|
||
previous_value = previous_day["ending_portfolio_value"]
|
||
daily_profit = current_value - previous_value
|
||
daily_return_pct = (daily_profit / previous_value) * 100
|
||
|
||
return {
|
||
"daily_profit": daily_profit,
|
||
"daily_return_pct": daily_return_pct,
|
||
"starting_portfolio_value": current_value,
|
||
"days_since_last_trading": calculate_day_gap(previous_day["date"], current_date)
|
||
}
|
||
```
|
||
|
||
### Key Insight: P&L from Price Changes, Not Trades
|
||
|
||
**Important:** Since all trades within a day use the same day's prices, portfolio value doesn't change between trades. P&L only changes when moving to the next day with new prices.
|
||
|
||
**Example:**
|
||
- Friday close: Hold 10 AAPL at $100 = $1000 total
|
||
- Monday open: AAPL now $110
|
||
- Monday P&L: 10 × ($110 - $100) = **+$100 profit**
|
||
- All Monday trades use $110 price, so P&L remains constant for that day
|
||
|
||
---
|
||
|
||
## Weekend/Holiday Handling
|
||
|
||
### Problem
|
||
|
||
Trading days are not consecutive calendar days:
|
||
- Friday → Monday (3-day gap)
|
||
- Before holidays (4+ day gaps)
|
||
|
||
### Solution
|
||
|
||
Use `ORDER BY date DESC LIMIT 1` to find **most recent trading day**, not just previous calendar date.
|
||
|
||
```sql
|
||
SELECT td_prev.id
|
||
FROM trading_days td_current
|
||
JOIN trading_days td_prev ON
|
||
td_prev.job_id = td_current.job_id AND
|
||
td_prev.model = td_current.model AND
|
||
td_prev.date < td_current.date
|
||
WHERE td_current.id = ?
|
||
ORDER BY td_prev.date DESC
|
||
LIMIT 1
|
||
```
|
||
|
||
This automatically handles:
|
||
- Normal weekdays (1 day gap)
|
||
- Weekends (3 day gap)
|
||
- Long weekends (4+ day gap)
|
||
|
||
---
|
||
|
||
## Reasoning Summary Generation
|
||
|
||
### When to Generate
|
||
|
||
**Timing:** After trading session completes, before storing final results.
|
||
|
||
### Implementation
|
||
|
||
```python
|
||
async def generate_reasoning_summary(reasoning_log, ai_model):
|
||
"""
|
||
Use same AI model to summarize its own trading decisions.
|
||
|
||
Prompt: "Summarize your trading strategy and key decisions in 2-3 sentences."
|
||
"""
|
||
try:
|
||
summary = await ai_model.ainvoke([{
|
||
"role": "user",
|
||
"content": build_summary_prompt(reasoning_log)
|
||
}])
|
||
return extract_content(summary)
|
||
|
||
except Exception as e:
|
||
# Fallback: Statistical summary
|
||
return f"Executed {trade_count} trades using {search_count} searches."
|
||
```
|
||
|
||
### Model Choice
|
||
|
||
**Use same model that did the trading** (Option A from brainstorming):
|
||
- Pro: Consistency, model summarizing its own reasoning
|
||
- Pro: Simpler configuration
|
||
- Con: Extra API cost per day (acceptable for quality)
|
||
|
||
---
|
||
|
||
## Unified Results API
|
||
|
||
### Endpoint Design
|
||
|
||
```
|
||
GET /results?job_id={id}&model={sig}&date={date}&reasoning={level}
|
||
```
|
||
|
||
**Parameters:**
|
||
- `job_id` (optional) - Filter by job
|
||
- `model` (optional) - Filter by model
|
||
- `date` (optional) - Filter by date
|
||
- `reasoning` (optional) - `none` (default), `summary`, `full`
|
||
|
||
### Response Structure
|
||
|
||
```json
|
||
{
|
||
"count": 2,
|
||
"results": [
|
||
{
|
||
"date": "2025-10-06",
|
||
"model": "gpt-5",
|
||
"job_id": "d8b52033-...",
|
||
|
||
"starting_position": {
|
||
"holdings": [
|
||
{"symbol": "AMZN", "quantity": 11},
|
||
{"symbol": "MSFT", "quantity": 10}
|
||
],
|
||
"cash": 100.0,
|
||
"portfolio_value": 9900.0
|
||
},
|
||
|
||
"daily_metrics": {
|
||
"profit": 57.96,
|
||
"return_pct": 0.585,
|
||
"days_since_last_trading": 1
|
||
},
|
||
|
||
"trades": [
|
||
{
|
||
"action_type": "buy",
|
||
"symbol": "NVDA",
|
||
"quantity": 12,
|
||
"price": 186.23,
|
||
"created_at": "2025-10-06T14:30:00Z"
|
||
}
|
||
],
|
||
|
||
"final_position": {
|
||
"holdings": [
|
||
{"symbol": "AMZN", "quantity": 11},
|
||
{"symbol": "MSFT", "quantity": 10},
|
||
{"symbol": "NVDA", "quantity": 12}
|
||
],
|
||
"cash": 114.86,
|
||
"portfolio_value": 9957.96
|
||
},
|
||
|
||
"metadata": {
|
||
"total_actions": 1,
|
||
"session_duration_seconds": 45.2,
|
||
"completed_at": "2025-10-06T14:31:00Z"
|
||
},
|
||
|
||
"reasoning": null // or summary string or full array
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### Reasoning Levels
|
||
|
||
**`reasoning=none`** (default)
|
||
- `"reasoning": null`
|
||
- Fastest, no DB lookup of reasoning fields
|
||
|
||
**`reasoning=summary`**
|
||
- `"reasoning": "Analyzed AAPL earnings. Bought 10 shares..."`
|
||
- Pre-generated AI summary (2-3 sentences)
|
||
|
||
**`reasoning=full`**
|
||
- `"reasoning": [{role: "assistant", content: "..."}, {...}]`
|
||
- Complete conversation log (JSON array)
|
||
|
||
---
|
||
|
||
## Implementation Flow
|
||
|
||
### Simulation Execution (per model-day)
|
||
|
||
```python
|
||
async def run_trading_session(date):
|
||
# 1. Get previous trading day data
|
||
previous_day = db.get_previous_trading_day(job_id, model, date)
|
||
|
||
# 2. Load today's prices
|
||
current_prices = get_prices_for_date(date)
|
||
|
||
# 3. Calculate daily P&L
|
||
pnl_metrics = calculate_daily_pnl(previous_day, date, current_prices)
|
||
|
||
# 4. Create trading_day record
|
||
trading_day_id = db.create_trading_day(
|
||
job_id, model, date,
|
||
starting_cash=cash,
|
||
starting_portfolio_value=pnl_metrics["starting_portfolio_value"],
|
||
daily_profit=pnl_metrics["daily_profit"],
|
||
daily_return_pct=pnl_metrics["daily_return_pct"],
|
||
# ... other fields
|
||
)
|
||
|
||
# 5. Run AI trading session
|
||
reasoning_log = []
|
||
for step in range(max_steps):
|
||
response = await ai_model.ainvoke(messages)
|
||
reasoning_log.append(response)
|
||
|
||
# Extract and execute trades
|
||
trades = extract_trades(response)
|
||
for trade in trades:
|
||
execute_trade(trade)
|
||
db.create_action(trading_day_id, trade)
|
||
|
||
if "<FINISH_SIGNAL>" in response:
|
||
break
|
||
|
||
# 6. Generate reasoning summary
|
||
summary = await generate_reasoning_summary(reasoning_log, ai_model)
|
||
|
||
# 7. Save final holdings
|
||
for symbol, quantity in holdings.items():
|
||
db.create_holding(trading_day_id, symbol, quantity)
|
||
|
||
# 8. Update trading_day with completion data
|
||
db.update_trading_day(
|
||
trading_day_id,
|
||
ending_cash=cash,
|
||
ending_portfolio_value=calculate_portfolio_value(),
|
||
reasoning_summary=summary,
|
||
reasoning_full=json.dumps(reasoning_log)
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
## Error Handling & Edge Cases
|
||
|
||
### First Trading Day
|
||
|
||
**Scenario:** No previous day exists
|
||
**Solution:** Return zero P&L, starting value = initial cash
|
||
|
||
```python
|
||
if previous_day is None:
|
||
return {
|
||
"daily_profit": 0.0,
|
||
"daily_return_pct": 0.0,
|
||
"starting_portfolio_value": initial_cash
|
||
}
|
||
```
|
||
|
||
### Weekend Gaps
|
||
|
||
**Scenario:** Friday → Monday (no trading Sat/Sun)
|
||
**Solution:** Query finds Friday as previous day automatically
|
||
**Metadata:** `days_since_last_trading: 3`
|
||
|
||
### Missing Price Data
|
||
|
||
**Scenario:** Holdings contain symbol with no price
|
||
**Solution:** Raise `ValueError` with clear message
|
||
|
||
```python
|
||
if symbol not in prices:
|
||
raise ValueError(f"Missing price data for {symbol} on {date}")
|
||
```
|
||
|
||
### Reasoning Summary Failure
|
||
|
||
**Scenario:** AI API fails when generating summary
|
||
**Solution:** Fallback to statistical summary
|
||
|
||
```python
|
||
return f"Executed {trade_count} trades using {search_count} searches. Full log available."
|
||
```
|
||
|
||
### Interrupted Trading Day
|
||
|
||
**Scenario:** Simulation crashes mid-day
|
||
**Solution:** Mark trading_day as failed, preserve partial actions for debugging
|
||
|
||
```python
|
||
db.execute("UPDATE trading_days SET status='failed', error_message=? WHERE id=?")
|
||
# Keep partial action records
|
||
```
|
||
|
||
---
|
||
|
||
## Migration Strategy
|
||
|
||
### Chosen Approach: Clean Break
|
||
|
||
**Decision:** Delete old `positions` table, start fresh with new schema.
|
||
|
||
**Rationale:**
|
||
- Simpler than data migration
|
||
- Acceptable for development phase
|
||
- Clean slate ensures no legacy issues
|
||
|
||
**Implementation:**
|
||
```python
|
||
def migrate_clean_database():
|
||
db.execute("DROP TABLE IF EXISTS positions")
|
||
create_trading_days_schema(db)
|
||
```
|
||
|
||
---
|
||
|
||
## Testing Strategy
|
||
|
||
### Unit Tests
|
||
|
||
- Daily P&L calculation logic
|
||
- First day (zero P&L)
|
||
- Positive/negative returns
|
||
- Weekend gaps
|
||
- Multiple holdings
|
||
- Database helper methods
|
||
- Create trading_day
|
||
- Get previous trading day
|
||
- Get starting/ending holdings
|
||
|
||
### Integration Tests
|
||
|
||
- BaseAgent P&L integration
|
||
- First day creates record with zero P&L
|
||
- Second day calculates P&L from price changes
|
||
- Results API
|
||
- Response structure
|
||
- Reasoning parameter variations
|
||
- Filtering by job_id, model, date
|
||
|
||
### End-to-End Tests
|
||
|
||
- Complete simulation workflow
|
||
- Multi-day simulation
|
||
- Verify holdings chain across days
|
||
- Verify P&L calculations
|
||
- Verify reasoning summaries
|
||
|
||
### Performance Tests
|
||
|
||
- Query speed with large datasets
|
||
- Reasoning inclusion impact on response time
|
||
|
||
---
|
||
|
||
## Success Criteria
|
||
|
||
✅ **Functional Requirements:**
|
||
1. Daily P&L shows non-zero values when portfolio changes
|
||
2. Weekend gaps handled correctly (finds Friday when starting Monday)
|
||
3. Results API returns day-centric structure
|
||
4. Reasoning available at 3 levels (none/summary/full)
|
||
5. Holdings chain correctly across days
|
||
6. First day shows zero P&L
|
||
|
||
✅ **Technical Requirements:**
|
||
1. Test coverage >85%
|
||
2. No data duplication (normalized schema)
|
||
3. API response time <2s for 100 days
|
||
4. Database auto-initializes new schema
|
||
5. Old positions table removed
|
||
|
||
✅ **Documentation:**
|
||
1. API reference updated
|
||
2. Database schema documented
|
||
3. Implementation plan created
|
||
4. Migration guide provided
|
||
|
||
---
|
||
|
||
## Implementation Estimate
|
||
|
||
**Total Time:** 8-12 hours for experienced developer
|
||
|
||
**Breakdown:**
|
||
- Task 1: Database schema migration (1-2h)
|
||
- Task 2: Database helpers (1h)
|
||
- Task 3: P&L calculator (1h)
|
||
- Task 4: Reasoning summarizer (1h)
|
||
- Task 5: BaseAgent integration (2h)
|
||
- Task 6: Results API endpoint (1-2h)
|
||
- Task 7-11: Testing, docs, cleanup (2-3h)
|
||
|
||
---
|
||
|
||
## Future Enhancements (Not in Scope)
|
||
|
||
- Historical P&L charts
|
||
- Configurable summary model (cheaper alternative)
|
||
- Streaming reasoning logs
|
||
- P&L breakdown by position
|
||
- Benchmarking against indices
|
||
|
||
---
|
||
|
||
## References
|
||
|
||
- Implementation Plan: `docs/plans/2025-11-03-daily-pnl-results-api-refactor.md`
|
||
- Database Schema: `docs/developer/database-schema.md`
|
||
- API Reference: `API_REFERENCE.md`
|
||
|
||
---
|
||
|
||
**Status:** ✅ Design Approved - Ready for Implementation
|
||
**Next Step:** Execute implementation plan task-by-task
|