using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using PolyTraderSharp.Models; namespace PolyTraderSharp.Services { public class SnapshotService : BackgroundService { private readonly TradingState _state; private readonly CopyTradingState _copyState; private readonly ILogger _logger; private readonly string _snapshotPath = "snapshot.json"; private readonly TimeSpan _interval = TimeSpan.FromSeconds(30); private readonly JobStatusRow _jobStatus; public SnapshotService(TradingState state, CopyTradingState copyState, ILogger logger, JobManager jobManager) { _state = state; _copyState = copyState; _logger = logger; _jobStatus = new JobStatusRow { JobName = "State Snapshot", Description = "Saves active application state (balances, open pos) to snapshot.json.", StatusText = "Pending Initial Delay..." }; _jobStatus.ManualTriggerAction = async () => { string oldStatus = _jobStatus.StatusText; _jobStatus.StatusText = "Running (Manual)..."; await SaveSnapshotAsync(); _jobStatus.StatusText = "Idle"; }; jobManager.RegisterJob(_jobStatus); } public override async Task StartAsync(CancellationToken cancellationToken) { // Load state on startup if (File.Exists(_snapshotPath)) { try { string json = await File.ReadAllTextAsync(_snapshotPath, cancellationToken); var snapshot = JsonConvert.DeserializeObject(json); if (snapshot != null) { _state.LiveTradingMode = snapshot.LiveMode; _state.DemoTradingMode = snapshot.DemoMode; _copyState.TotalCopyTrades = snapshot.CopyTrades; _state.GlobalPnl = snapshot.GlobalPnl; int restoredPositions = 0; // Restore OpenPositions to matching accounts foreach (var kvp in snapshot.OpenPositions) { if (_state.Accounts.TryGetValue(kvp.Key, out var acc)) { foreach (var pos in kvp.Value) { acc.OpenPositions.TryAdd(pos.Key, pos.Value); restoredPositions++; } } } _logger.LogInformation($"Snapshot loaded. Restored {restoredPositions} positions. Modes: Live={snapshot.LiveMode}, Demo={snapshot.DemoMode}"); } } catch (Exception ex) { _logger.LogError(ex, "Failed to load snapshot on startup"); } } await base.StartAsync(cancellationToken); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _jobStatus.StatusText = "Idle"; while (!stoppingToken.IsCancellationRequested) { if (_jobStatus.IsEnabled) { try { _jobStatus.StatusText = "Running (Scheduled)..."; await SaveSnapshotAsync(); _jobStatus.LastRun = DateTime.Now; } catch (TaskCanceledException) { break; } catch (Exception ex) { _logger.LogError(ex, "Error saving TradingState snapshot"); _jobStatus.StatusText = "Error!"; } finally { if (_jobStatus.StatusText != "Error!") _jobStatus.StatusText = "Idle"; } } else { _jobStatus.StatusText = "Paused"; } _jobStatus.NextRun = DateTime.Now.Add(_interval); await Task.Delay(_interval, stoppingToken); } } private async Task SaveSnapshotAsync() { var snapshot = new StateSnapshot { LiveMode = _state.LiveTradingMode, DemoMode = _state.DemoTradingMode, CopyTrades = _copyState.TotalCopyTrades, GlobalPnl = _state.GlobalPnl, OpenPositions = _state.Accounts.ToDictionary( a => a.Key, a => a.Value.OpenPositions.ToDictionary(p => p.Key, p => p.Value) ) }; string json = JsonConvert.SerializeObject(snapshot, Formatting.Indented); await File.WriteAllTextAsync(_snapshotPath, json); _logger.LogTrace("TradingState snapshot saved."); } private class StateSnapshot { public TradingMode LiveMode { get; set; } public TradingMode DemoMode { get; set; } public int CopyTrades { get; set; } public decimal GlobalPnl { get; set; } public Dictionary> OpenPositions { get; set; } = new(); } } }