- Core TradingState (in Core): globale Schalter, Accounts, MarketCache, GlobalPnl. - Neuer CopyTradingState (im Modul): Traders, MasterTraderPositions, TraderAnalyticsCache, TotalCopyTrades/GetNextTradeId, PendingOrderTimestamps, SixSharesMinimum. - 10 Konsumenten umgestellt (Program, frm_main, CopyTradingEngine, TraderMonitor, Alchemy, WSS, Snapshot, StartupHydration, beide Analytics-Jobs): Modul-Felder von _state.* auf _copyState.* umgeleitet, CopyTradingState via DI. - Rein mechanische Feld-Umleitung, keine Logikänderung. Build 0 Fehler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
153 lines
5.7 KiB
C#
153 lines
5.7 KiB
C#
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<SnapshotService> _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<SnapshotService> logger, JobManager jobManager)
|
|
{
|
|
_state = state;
|
|
_copyState = copyState;
|
|
_logger = logger;
|
|
|
|
_jobStatus = new JobStatusRow
|
|
{
|
|
JobName = "LiteDB 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<StateSnapshot>(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<int, Dictionary<string, Position>> OpenPositions { get; set; } = new();
|
|
}
|
|
}
|
|
}
|