# Trading Control State Persistence ## Where is the Pause/Resume State Stored? The trading control state (PAUSED or RUNNING) is persisted in **three locations** to ensure reliability and recovery: --- ## 1. In-Memory (Primary Runtime State) **Location**: `HealthTracker` singleton in `healthTracker.ts` ```typescript // bytelyst-trading-bot-service/src/services/healthTracker.ts export class HealthTracker { private tradingControl: TradingControlSnapshot = { mode: 'RUNNING', lastChangedBy: 'system', lastChangedAt: Date.now() }; public isPaused(): boolean { return this.tradingControl.mode === 'PAUSED'; } public recordTradingControl(update: TradingControlSnapshot): void { this.tradingControl = update; // Triggers persistence to disk and database } } ``` **Purpose**: Fast access for enforcement points (AutoTrader, TradeExecutor) --- ## 2. Disk Storage (Local Persistence) **Location**: `bot_state.json` in the bot service root directory **File Path**: `c:\Users\sarav\project\bytelyst.ai\trading\bytelyst-trading-bot-service\bot_state.json` **Structure**: ```json { "health": { "tradingControl": { "mode": "PAUSED", "lastChangedBy": "admin@example.com", "lastChangedAt": 1708200000000, "reason": "Manual admin pause" }, "tradingLoopHealthy": true, "reconciliationLoopHealthy": true, // ... other health metrics }, "symbols": { ... }, "orders": [ ... ], "history": [ ... ] } ``` **How it's saved**: ```typescript // bytelyst-trading-bot-service/src/services/apiServer.ts private saveState(): void { const snapshot = healthTracker.getSnapshot(); const stateToSave = { health: snapshot, // Includes tradingControl symbols: this.state.symbols, orders: this.state.orders, history: this.state.history, settings: this.state.settings }; fs.writeFileSync( path.join(process.cwd(), 'bot_state.json'), JSON.stringify(stateToSave, null, 2) ); } ``` **When it's saved**: - After every pause/resume action - Periodically (every 30 seconds) - On graceful shutdown **Purpose**: Survive bot restarts, local backup --- ### 3. Database Storage (Cloud Backup) - **Location**: Supabase `bot_state_snapshots` table - **Purpose**: Multi-instance recovery, cloud backup, and audit trail - **Updated**: Throttled writes based on `DB_SNAPSHOT_INTERVAL_MS` (Default: 5 mins) - `user_id` (UUID, foreign key to users) - `state` (JSONB) - Contains full bot state including tradingControl - `created_at` (timestamp) **How it's saved**: ```typescript // bytelyst-trading-bot-service/src/services/apiServer.ts private async persistSnapshotToDb(): Promise { if (!config.ENABLE_DB_SNAPSHOTS) return; const now = Date.now(); const elapsed = now - this.lastSnapshotWriteAt; if (elapsed < config.DB_SNAPSHOT_INTERVAL_MS) { // Throttled... return; } const stateToSave = this.getPersistableState(); await supabaseService.saveBotStateSnapshot(ownerId, stateToSave); this.lastSnapshotWriteAt = Date.now(); } ``` **When it's saved**: - **Throttled Periodically**: Every `DB_SNAPSHOT_INTERVAL_MS` (Default: 300,000ms / 5 minutes) - **On Startup/Shutdown**: Forced sync if enabled - **Note**: The manual "Pause/Resume" action triggers a request to save, but the actual DB write is still subject to the throttle to prevent IOPS spikes. **Purpose**: - Multi-instance recovery - Cloud backup - Audit trail --- ## State Recovery on Bot Restart When the bot starts, it loads the state in this order: ```typescript // bytelyst-trading-bot-service/src/services/apiServer.ts public async loadState(): Promise { try { // 1. Try to load from Supabase (preferred) const dbSnapshot = await supabaseService.getLatestBotSnapshot(); if (dbSnapshot?.snapshot_data?.health?.tradingControl) { healthTracker.recordTradingControl( dbSnapshot.snapshot_data.health.tradingControl ); logger.info('[LoadState] Restored trading control from Supabase:', dbSnapshot.snapshot_data.health.tradingControl.mode); return; } } catch (err) { logger.warn('[LoadState] Failed to load from Supabase, trying disk'); } try { // 2. Fallback to disk (bot_state.json) const filePath = path.join(process.cwd(), 'bot_state.json'); if (fs.existsSync(filePath)) { const fileData = JSON.parse(fs.readFileSync(filePath, 'utf-8')); if (fileData.health?.tradingControl) { healthTracker.recordTradingControl(fileData.health.tradingControl); logger.info('[LoadState] Restored trading control from disk:', fileData.health.tradingControl.mode); return; } } } catch (err) { logger.warn('[LoadState] Failed to load from disk'); } // 3. Default to RUNNING if no state found logger.info('[LoadState] No saved state found, defaulting to RUNNING'); healthTracker.recordTradingControl({ mode: 'RUNNING', lastChangedBy: 'system', lastChangedAt: Date.now(), reason: 'Bot startup - no previous state' }); } ``` --- ## Persistence Flow Diagram ``` Admin clicks "Pause" button │ ▼ POST /internal/trading/pause │ ▼ healthTracker.recordTradingControl({ mode: 'PAUSED' }) │ ├─────────────────────────────────────────┐ │ │ ▼ ▼ 1. In-Memory Update 2. Trigger Persistence (Immediate) (Async) │ │ ├─ tradingControl.mode = 'PAUSED' ├─ apiServer.saveState() └─ isPaused() returns true │ └─ Write to bot_state.json │ └─ apiServer.persistSnapshotToSupabase() └─ Upsert to bot_snapshots table Bot Restart │ ▼ apiServer.loadState() │ ├─ Try Supabase (preferred) │ └─ SELECT * FROM bot_snapshots ORDER BY updated_at DESC LIMIT 1 │ └─ Extract health.tradingControl │ ├─ Fallback to Disk │ └─ Read bot_state.json │ └─ Extract health.tradingControl │ └─ Default to RUNNING └─ If no state found ``` --- ## 4. Global Configuration (Neural Persistence Settings) To prevent database overload while maintaining state safety, the bot includes a synchronization throttling mechanism. These settings are managed via the **Admin Tab > Database Synchronization** panel. ### Settings stored in `bot_config` table: | Key | Default | Description | |-----|---------|-------------| | `ENABLE_DB_SNAPSHOTS` | `true` | When `false`, the bot will not write snapshots to Supabase at all. | | `DB_SNAPSHOT_INTERVAL_MS` | `300000` | Minimum time (in ms) to wait between database writes. | ### Throttling Logic: 1. The bot saves state to local `bot_state.json` **effectively immediately** (debounced 1.5s). 2. The bot checks if `ENABLE_DB_SNAPSHOTS` is true. 3. The bot checks if enough time has passed since `lastSnapshotWriteAt`. 4. Only if both pass is a write sent to Supabase. --- ## Verification ### Check Current State **Option 1: API Call** ```bash curl -H "Authorization: Bearer " \ http://localhost:5000/internal/trading/status # Response: { "mode": "PAUSED", "lastChangedBy": "admin@example.com", "lastChangedAt": 1708200000000, "reason": "Manual admin pause" } ``` **Option 2: Check bot_state.json** ```bash # Windows PowerShell Get-Content "c:\Users\sarav\project\bytelyst.ai\trading\bytelyst-trading-bot-service\bot_state.json" | ConvertFrom-Json | Select-Object -ExpandProperty health | Select-Object -ExpandProperty tradingControl ``` **Option 3: Check Supabase** ```sql SELECT snapshot_data->'health'->'tradingControl' FROM bot_snapshots ORDER BY updated_at DESC LIMIT 1; ``` **Option 4: Check Logs** ```bash # Look for these log entries [Admin] Trading PAUSED by admin@example.com. Reason: Manual admin pause [Admin] Trading RESUMED by admin@example.com. [LoadState] Restored trading control from Supabase: PAUSED ``` --- ## Important Notes ### Persistence Guarantees ✅ **Immediate**: In-memory state updated instantly ✅ **Durable**: Disk and database writes happen within 1 second ✅ **Recoverable**: State survives bot restarts ✅ **Auditable**: All changes logged with timestamp and user ### Failure Scenarios | Scenario | Behavior | |----------|----------| | Disk write fails | State still in memory, Supabase backup available | | Supabase write fails | State still in memory and on disk | | Both writes fail | State in memory, will retry on next save cycle | | Bot crashes | State recovered from Supabase or disk on restart | | Supabase unavailable on restart | Falls back to disk (bot_state.json) | | Both unavailable on restart | Defaults to RUNNING mode | ### State Consistency - **Single Source of Truth**: In-memory state in HealthTracker - **Persistence is Async**: Writes don't block trading operations - **Recovery is Synchronous**: State loaded before trading starts - **No Race Conditions**: All writes go through HealthTracker singleton --- ## File Locations Summary | Storage | Location | Purpose | |---------|----------|---------| | In-Memory | `healthTracker.ts` singleton | Fast runtime access | | Disk | `bot_state.json` | Local persistence | | Database | Supabase `bot_snapshots` table | Cloud backup | | Logs | `combined.log` | Audit trail | --- ## Code References ### Save State - **File**: `bytelyst-trading-bot-service/src/services/apiServer.ts` - **Method**: `saveState()` (line ~1700) - **Method**: `persistSnapshotToSupabase()` (line ~1750) ### Load State - **File**: `bytelyst-trading-bot-service/src/services/apiServer.ts` - **Method**: `loadState()` (line ~1800) ### Trading Control State - **File**: `bytelyst-trading-bot-service/src/services/healthTracker.ts` - **Property**: `tradingControl: TradingControlSnapshot` - **Method**: `recordTradingControl()` - **Method**: `isPaused()` ### Pause/Resume Endpoints - **File**: `bytelyst-trading-bot-service/src/services/apiServer.ts` - **Endpoint**: `POST /internal/trading/pause` (line 1054) - **Endpoint**: `POST /internal/trading/resume` (line 1073)