# OTB-236: RecursionError und RuntimeError beim OptraBot Shutdown mit Tastytrade

## Problem

Beim Shutdown des OptraBots (Ctrl+C) traten folgende Fehler auf:

### 1. RecursionError (494 Rekursionsebenen!)
```python
RecursionError: maximum recursion depth exceeded
[Previous line repeated 494 more times]
```

### 2. RuntimeError - Event Loop bereits geschlossen
```python
RuntimeError: no running event loop
Exception in callback Task.task_wakeup(<Future cancelled>)
```

### 3. Coroutine nicht awaited
```python
RuntimeWarning: coroutine 'TastytradeConnector._on_streamer_disconnect' was never awaited
```

## Root Cause Analyse

### Shutdown-Sequenz (Log vom 30.10.2025, 02:27:41):
```
02:27:41.674 | INFO  | Disconnecting from broker TASTY
02:27:41.674 | DEBUG | Trading enabled: False - Reason: Broker disconnected
02:27:41.674 | DEBUG | Tastytrade Streamer disconnected. Performing disconnect operations
02:27:41.787 | DEBUG | Cancelled listening to quotes and greeks
02:27:42.197 | INFO  | OptraBot shutdown completed
[Event Loop wird geschlossen]
[Tastytrade DXLinkStreamer versucht noch Tasks zu erstellen]
❌ RuntimeError: no running event loop
❌ RecursionError durch verschachtelte Task-Cancels
```

### Das Problem im Code:

**Alte Implementierung (`_on_streamer_disconnect`):**
```python
async def _on_streamer_disconnect(self, streamer: DXLinkStreamer):
    if self._is_disconnecting == False:
        self._is_disconnecting = True
        await self._disconnect_internal()  # ← PROBLEM!
```

**`_disconnect_internal()` macht:**
```python
async def _disconnect_internal(self):
    if self._streamer:
        await self._streamer.close()  # ← Ruft disconnect_fn auf!
```

**Rekursions-Kette:**
1. `disconnect()` wird aufgerufen (Shutdown)
2. → ruft `_disconnect_internal()` auf
3. → ruft `streamer.close()` auf
4. → Tastytrade Library ruft `disconnect_fn` auf
5. → `_on_streamer_disconnect()` wird aufgerufen
6. → ruft wieder `_disconnect_internal()` auf
7. → **ZURÜCK ZU SCHRITT 3** → Infinite Loop!

**Zusätzlich:** Die Tastytrade-Library ruft den Callback während/nach Event Loop Shutdown auf, wenn bereits keine Event Loop mehr läuft.

## Lösung

### Änderungen in `tastytradeconnector.py::_on_streamer_disconnect()`:

#### 1. Event Loop Status Check
```python
# OTB-236: Check if event loop is still running
try:
    loop = asyncio.get_running_loop()
except RuntimeError:
    logger.debug("Event loop not running, ignoring callback")
    return

if loop.is_closing():
    logger.debug("Event loop is closing, ignoring callback")
    return
```

#### 2. Frühes Return bei aktivem Disconnect
```python
if self._is_disconnecting:
    logger.debug("Already disconnecting, ignoring callback")
    return
```

#### 3. **KEIN** Aufruf von `_disconnect_internal()`
```python
# OTB-236: Don't call _disconnect_internal() here!
# It would try to close the streamer again → recursion!

# Instead: manually clean up
await self.set_trading_enabled(False, "Streamer disconnected")

# Cancel tasks
for task in tasks_to_cancel:
    if task and not task.done():
        task.cancel()

# Clear references (don't call close()!)
self._streamer = None
self._alert_streamer = None

# Emit event for reconnection logic
self._emitDisconnectedEvent()
```

## Erwartetes Verhalten nach Fix

### Normaler Shutdown:
```
02:27:41.674 | Disconnecting from broker TASTY
02:27:41.674 | Tastytrade Streamer disconnect callback triggered
02:27:41.674 | Already disconnecting, ignoring streamer disconnect callback
02:27:41.787 | Cancelled listening to quotes and greeks
02:27:42.197 | OptraBot shutdown completed
✅ Kein RecursionError
✅ Kein RuntimeError
✅ Sauberer Shutdown
```

### Unexpected Disconnect (während des Betriebs):
```
11:30:00.123 | Tastytrade Streamer disconnect callback triggered
11:30:00.123 | Unexpected DXLink streamer disconnect detected
11:30:00.123 | Initiating full reconnection...
11:30:00.124 | Trading enabled: False - Reason: Streamer disconnected
11:30:00.125 | Cancelled streaming tasks
11:30:00.126 | Emitting disconnected event
✅ Sauberer Disconnect ohne Rekursion
✅ Reconnection Logic wird getriggert
```

## Testing

### Zu testen:
1. ✅ **Normaler Shutdown (Ctrl+C)** - Keine Exceptions
2. ✅ **Unexpected Streamer Disconnect** - Saubere Reconnection
3. ✅ **Mehrfache Disconnect Callbacks** - Werden korrekt ignoriert
4. ✅ **Event Loop Shutdown** - RuntimeError wird verhindert

### Zu beobachten im Log:
- Keine `RecursionError` mehr
- Keine `RuntimeError: no running event loop`
- Keine `RuntimeWarning: coroutine was never awaited`
- Saubere Shutdown-Sequenz

## Dateien geändert

- `optrabot/broker/tastytradeconnector.py` - `_on_streamer_disconnect()` Methode

## Verwandte Issues

- OTB-253: Trade Recovery (Recovery-Logik wurde nicht durch dieses Issue betroffen)
- OTB-254: EOD Settlement (Unabhängig von diesem Fix)

## Test-Plan

### Test 1: Normaler Shutdown (Ctrl+C)
**Durchführung:**
1. OptraBot mit Tastytrade-Verbindung starten
2. Warten auf "Broker TASTY connected successfully"
3. Warten bis Streaming aktiv ist (Quote-Updates im Log sichtbar)
4. Ctrl+C drücken für Shutdown

**Erwartete Ergebnisse:**
- ✓ "Disconnecting from broker TASTY"
- ✓ "Tastytrade Streamer disconnect callback triggered"
- ✓ "Already disconnecting, ignoring streamer disconnect callback"
- ✓ "OptraBot shutdown completed"
- ✓ **KEINE** RecursionError
- ✓ **KEINE** RuntimeError: no running event loop
- ✓ **KEINE** RuntimeWarning: coroutine was never awaited

### Test 2: Unexpected Streamer Disconnect
**Durchführung:**
1. OptraBot mit Tastytrade starten
2. Warten bis Streaming aktiv
3. Netzwerkverbindung kurz unterbrechen (z.B. WiFi aus/an)
4. Warten auf Streamer-Disconnect-Callback
5. Netzwerk wieder aktivieren

**Erwartete Ergebnisse:**
- ✓ "Unexpected DXLink streamer disconnect detected"
- ✓ "Initiating full reconnection..."
- ✓ "Trading enabled: False - Reason: Streamer disconnected"
- ✓ Broker emittiert disconnected Event
- ✓ Reconnection-Versuch nach Delay
- ✓ Keine Recursion-Errors

### Test 3: Multiple Disconnect Callbacks (Edge Case)
**Durchführung:**
1. OptraBot mit Tastytrade starten
2. Shutdown initiieren
3. Beobachten ob Streamer-Callback mehrfach getriggert wird

**Erwartete Ergebnisse:**
- ✓ Erster Callback: "Already disconnecting, ignoring..."
- ✓ Weitere Callbacks: Ebenfalls ignoriert
- ✓ Sauberer Shutdown ohne Errors

### Test 4: Shutdown während aktivem Trading
**Durchführung:**
1. OptraBot mit Tastytrade starten
2. Trade platzieren (oder aktiven Trade haben)
3. Während Trade aktiv ist: Ctrl+C

**Erwartete Ergebnisse:**
- ✓ TradeManager shutdown zuerst
- ✓ Danach Broker-Disconnect
- ✓ Keine Exceptions während Shutdown
- ✓ Trade-Status in Datenbank erhalten

### Test 5: Overnight Run (Real-World Test)
**Durchführung:**
1. OptraBot abends starten
2. Über Nacht mit Tastytrade-Verbindung laufen lassen
3. Morgens mit Ctrl+C beenden

**Erwartete Ergebnisse:**
- ✓ Keine Exceptions während Nacht-Betrieb
- ✓ Sauberer Shutdown am Morgen
- ✓ Keine Memory Leaks oder Zombie-Tasks

## Log-Analyse Checkliste

### RED FLAGS (sollten NICHT erscheinen):
- ✗ `RecursionError: maximum recursion depth exceeded`
- ✗ `RuntimeError: no running event loop`
- ✗ `RuntimeWarning: coroutine 'TastytradeConnector._on_streamer_disconnect' was never awaited`
- ✗ `Exception in callback Task.task_wakeup`
- ✗ `Task was destroyed but it is pending`

### GREEN FLAGS (sollten erscheinen):
- ✓ `Disconnecting from broker TASTY`
- ✓ `Tastytrade Streamer disconnect callback triggered`
- ✓ `Already disconnecting, ignoring streamer disconnect callback` ODER
- ✓ `Event loop not running, ignoring callback` ODER
- ✓ `Event loop is closing, ignoring callback`
- ✓ `OptraBot shutdown completed`

## Performance-Metriken

### 1. Shutdown-Dauer
- **Messung:** Zeit von "Shutting down OptraBot" bis "OptraBot shutdown completed"
- **Ziel:** < 2 Sekunden

### 2. Task-Cancellation-Zeit
- **Messung:** Zeit von "Cancelled listening to quotes" bis Completion
- **Ziel:** < 1 Sekunde

### 3. Streamer-Close-Zeit
- **Messung:** Timeout-Logs prüfen (sollten nicht erscheinen)
- **Ziel:** Keine Timeout-Warnings

## Zusätzliche Validierung

### 1. Process-Cleanup prüfen
```bash
ps aux | grep optrabot
# → Sollte leer sein nach Shutdown
```

### 2. Zombie-Tasks prüfen
- Keine "Task was destroyed but it is pending" Meldungen im Log

### 3. Datenbank-Integrität prüfen
- Trades-Tabelle konsistent
- Keine unvollständigen Transaktionen

## Regression Tests

**Diese Funktionen müssen weiterhin funktionieren:**
1. ✓ Normale Tastytrade-Verbindung beim Start
2. ✓ Quote/Greeks-Streaming
3. ✓ Trade-Execution
4. ✓ Reconnection nach Netzwerkproblemen
5. ✓ IBTWS-Verbindung (sollte unberührt sein)
6. ✓ Multi-Broker-Betrieb (TASTY + IBTWS)

## Lessons Learned

1. **Callback-Functions sollten nie rekursive Operationen triggern**
   - Disconnect-Callbacks dürfen nicht `close()` aufrufen, das sie ja vom `close()` aufgerufen werden

2. **Event Loop Status immer prüfen bei Callbacks**
   - Externe Libraries können Callbacks während Shutdown aufrufen
   - `asyncio.get_running_loop()` + `loop.is_closing()` prüfen

3. **Flags SOFORT setzen, nicht später**
   - `_is_disconnecting` Flag VOR allen Operationen setzen
   - Verhindert Race Conditions und rekursive Calls

4. **Cleanup ohne Library-Calls**
   - Bei Callbacks: Nur lokale Cleanup-Operationen
   - Keine Library-Methoden aufrufen die wieder Callbacks triggern könnten
