# OTB-259: EOD Settlement SQLAlchemy Relationship Fix

## Problem

Beim EOD Settlement am 24. Oktober 2025 trat folgender Fehler auf:

```
AssertionError: Dependency rule on column 'trades.id' tried to blank-out primary key column 'transactions.tradeid' on instance '<Transaction at 0x1fbd8b1d3d0>'
```

**Vollständiger Stack Trace:**
```python
File "optrabot\trademanager.py", line 942, in _performEODSettlement
    crud.update_trade(session, managedTrade.trade)
File "optrabot\crud.py", line 59, in update_trade
    session.commit()
...
AssertionError: Dependency rule on column 'trades.id' tried to blank-out primary key column 'transactions.tradeid'
```

## Ursache

Das Problem entstand durch **Session-Inkonsistenz** bei der EOD Settlement-Verarbeitung:

### Code-Flow (vorher - fehlerhaft):

```python
# In _performEODSettlement():
TradeHelper.updateTrade(managedTrade.trade)  # (1)

managedTrade.status = TradeStatus.EXPIRED
managedTrade.trade.status = TradeStatus.EXPIRED
with Session(get_db_engine()) as session:     # (2)
    crud.update_trade(session, managedTrade.trade)  # (3)
```

**Problem-Analyse:**

1. `TradeHelper.updateTrade()` erstellt eine **eigene Session** für EXPIRED Trades:
   ```python
   # In TradeHelper.updateTrade():
   if trade.status == TradeStatus.EXPIRED:
       with SessionLocal() as session:
           # Creates closing transactions
           exp_transaction = models.Transaction(...)
           session.add(exp_transaction)
           session.commit()  # Commits in SEPARATE session
   ```

2. Das `managedTrade.trade` Objekt ist nun **detached** von jeder Session

3. `crud.update_trade()` wird mit einer **neuen Session** und dem detached Trade-Objekt aufgerufen

4. SQLAlchemy's `session.merge()` versucht das Trade-Objekt mit seinen Relationships zu synchronisieren

5. Die neu erstellten `Transaction` Objekte sind aber in einer **anderen Session** committed worden

6. SQLAlchemy erkennt Inkonsistenzen in der `trade.transactions` Relationship und versucht die Foreign Keys zu "clearen"

7. **AssertionError**: Kann Primary Key Column `transactions.tradeid` nicht auf NULL setzen

### Warum trat das Problem erst jetzt auf?

Das Problem hängt mit **OTB-254** zusammen, wo die Logic für EXPIRED Trades geändert wurde:

**OTB-254 Änderung** (in TradeHelper.updateTrade):
```python
# For EXPIRED trades: Create closing transactions for open positions
if trade.status == TradeStatus.EXPIRED:
    with SessionLocal() as session:  # <-- NEUE Session
        # Create closing transactions
        session.commit()
```

**Vorher** wurden EXPIRED Trades ähnlich behandelt wie CLOSED Trades - ohne zusätzliche Session.

**Nach OTB-254** wird für EXPIRED Trades eine separate Session erstellt, was die Session-Inkonsistenz verursacht.

## Lösung

**Trade-Objekt nach `updateTrade()` neu laden**, um ein frisches, sauber an die Session gebundenes Objekt zu erhalten:

```python
# OTB-259 Fix:
managedTrade.trade.status = TradeStatus.EXPIRED

# Update PNL (creates closing transactions in separate session)
TradeHelper.updateTrade(managedTrade.trade)

# OTB-259: Reload trade from database to get fresh instance
with Session(get_db_engine()) as session:
    fresh_trade = crud.getTrade(session, managedTrade.trade.id)
    if fresh_trade:
        managedTrade.trade = fresh_trade  # Replace with fresh instance
        managedTrade.status = TradeStatus.EXPIRED
        crud.update_trade(session, fresh_trade)
    else:
        logger.error(f'Could not reload trade {managedTrade.trade.id}')
```

### Warum funktioniert das?

1. ✅ `crud.getTrade()` lädt das Trade-Objekt **frisch** aus der Datenbank
2. ✅ Das neue Objekt ist **korrekt an die Session gebunden**
3. ✅ Alle Relationships (`transactions`) sind **konsistent** mit dem DB-Zustand
4. ✅ `crud.update_trade()` kann sauber mergen ohne Relationship-Konflikte

## Geänderte Dateien

### `optrabot/trademanager.py`

**Methode:** `_performEODSettlement()`

**Änderungen:**
- Status wird **vor** `updateTrade()` auf EXPIRED gesetzt (damit die Closing Transactions erstellt werden)
- Nach `updateTrade()` wird das Trade-Objekt **neu geladen**
- Nur das frische Objekt wird an `crud.update_trade()` übergeben

## Testing

### Manueller Test

**Szenario:** Trade mit offenen Positionen läuft am Expiration Day aus

1. Trade mit Entry Order gefüllt
2. Keine Exit Order (oder nur teilweise gefüllt)
3. EOD Settlement wird ausgeführt
4. Trade sollte EXPIRED Status erhalten
5. Closing Transactions sollten erstellt werden
6. Kein AssertionError sollte auftreten

### Unit Test

```python
def test_eod_settlement_expired_trade():
    """Test EOD settlement for expired trade with open positions"""
    # Setup: Create trade with filled entry, no exit
    trade = create_test_trade(status=TradeStatus.OPEN)
    
    # Execute EOD settlement
    await trademanager._performEODSettlement()
    
    # Verify: Trade is EXPIRED, closing transactions created, no errors
    assert trade.status == TradeStatus.EXPIRED
    assert len(trade.transactions) > initial_tx_count
```

## Potential Future Improvements

### 1. Session Management Vereinheitlichen

Aktuell werden Sessions an verschiedenen Stellen erstellt:
- `TradeHelper.updateTrade()` - SessionLocal()
- `_performEODSettlement()` - Session(get_db_engine())
- Verschiedene andere Stellen

**Verbesserung:** Einheitliches Session-Management-Pattern

### 2. Trade Update Atomic Machen

```python
# Potential improvement:
def settle_expired_trade(trade_id: int) -> models.Trade:
    """Atomic operation to settle expired trade"""
    with Session(get_db_engine()) as session:
        trade = session.get(models.Trade, trade_id)
        trade.status = TradeStatus.EXPIRED
        
        # Create closing transactions in SAME session
        create_closing_transactions(session, trade)
        
        # Calculate PNL in SAME session
        calculate_pnl(trade)
        
        session.commit()
        return trade
```

### 3. Relationship Cascade Options prüfen

```python
class Trade(Base):
    transactions = relationship(
        "Transaction", 
        back_populates="trade",
        cascade="all, delete-orphan"  # Review cascade settings
    )
```

## Beziehung zu anderen Issues

- **OTB-254**: PNL Correction - führte die EXPIRED Trade Logic ein, die dieses Problem verursachte
- **OTB-252**: Trade Groups - ähnliche Session-Management-Patterns
- **OTB-256**: TWS Version Check - enthielt ebenfalls Session-Management-Fixes

## Lessons Learned

1. **Session-Konsistenz ist kritisch** bei SQLAlchemy Relationships
2. **Detached Objects** können zu subtilen Fehlern führen
3. **Mehrere Sessions** in einem Workflow erfordern besondere Vorsicht
4. **Reload nach externen Changes** ist oft sicherer als Merge
5. **Testing mit EXPIRED Trades** muss in Testsuiten aufgenommen werden

---

**Implementiert:** 25. Oktober 2025  
**Version:** 0.18.0a7  
**Status:** ✅ Fixed
