From f8d395b2a3ba99afbe14862ee1fa0b16f926d432 Mon Sep 17 00:00:00 2001 From: Richard Date: Wed, 1 Jul 2026 20:09:50 +0200 Subject: [PATCH] Phase 5.2: TradingState-Split (Core-State vs. CopyTradingState) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Program.cs | 7 ++- TradingState.cs | 63 ------------------- frm_main.cs | 32 +++++----- services/AlchemyWebsocketService.cs | 9 ++- services/CopyTradingEngine.cs | 25 ++++---- services/MasterTraderAnalyticsJob.cs | 6 +- services/PolymarketWssClient.cs | 5 +- services/SnapshotService.cs | 8 ++- services/StartupHydrationService.cs | 7 ++- services/TraderAnalyticsJob.cs | 8 ++- services/TraderMonitorService.cs | 45 ++++++------- src/PolyTrader.Core/TradingState.cs | 39 ++++++++++++ .../CopyTradingState.cs | 47 ++++++++++++++ 13 files changed, 174 insertions(+), 127 deletions(-) delete mode 100644 TradingState.cs create mode 100644 src/PolyTrader.Core/TradingState.cs create mode 100644 src/PolyTrader.Modules.CopyTrading/CopyTradingState.cs diff --git a/Program.cs b/Program.cs index 0c93f17..7f0df7d 100644 --- a/Program.cs +++ b/Program.cs @@ -37,6 +37,7 @@ internal static class Program services.AddCorePersistence(); services.AddSingleton((IServiceProvider sp) => ServerSettings.Load("server_settings.xml")); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(copySignalChannel.Writer); services.AddSingleton(copySignalChannel.Reader); services.AddSingleton(closedTradeChannel.Writer); @@ -102,16 +103,16 @@ internal static class Program } catch { } - var state = AppHost.Services.GetRequiredService(); + var copyState = AppHost.Services.GetRequiredService(); var maxTradeDoc = db.GetCollection("closed_trades") .Find(Builders.Filter.Empty) .SortByDescending(d => d["_id"]) .Limit(1) .FirstOrDefault(); - + if (maxTradeDoc != null && maxTradeDoc.Contains("_id")) { - state.TotalCopyTrades = maxTradeDoc["_id"].AsInt32; + copyState.TotalCopyTrades = maxTradeDoc["_id"].AsInt32; } } catch { } diff --git a/TradingState.cs b/TradingState.cs deleted file mode 100644 index 9849e95..0000000 --- a/TradingState.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Concurrent; -using PolyTraderSharp.Models; - -namespace PolyTraderSharp -{ - public enum TradingMode - { - Inactive, - SellOnly, - Active - } - - /// - /// In-Memory Hot-Path State for PolyTrader. - /// Replaces database lookups for core trading logic. - /// - public class TradingState - { - // Settings - public bool GlobalTradingPaused { get; set; } = false; - public TradingMode LiveTradingMode { get; set; } = TradingMode.Inactive; - public TradingMode DemoTradingMode { get; set; } = TradingMode.Inactive; - public bool IsAlchemyHealthy { get; set; } = false; - public bool EnableBlockchainParser { get; set; } = true; - public bool DebugPollingLog { get; set; } = false; - public bool DebugOrderPayloadLog { get; set; } = false; - public bool SixSharesMinimum { get; set; } = true; - - // Accounts (AccountId -> State) - public ConcurrentDictionary Accounts { get; } = new(); - - // Tracked Traders (TraderId -> TrackedTrader) - public ConcurrentDictionary Traders { get; } = new(); - - private int _totalCopyTrades = 0; - public int TotalCopyTrades - { - get => _totalCopyTrades; - set => _totalCopyTrades = value; - } - - public int GetNextTradeId() - { - return Interlocked.Increment(ref _totalCopyTrades); - } - - public decimal GlobalPnl { get; set; } = 0.0m; - // Analytics Cache (AccountId -> List) - public ConcurrentDictionary> TraderAnalyticsCache { get; } = new(); - - // Tracks when live orders were placed for stale order cleanup - // Key: "AccountId_TokenId", Value: (PlacedAt, SourceTraderId) - public ConcurrentDictionary PendingOrderTimestamps { get; } = new(); - - // High-Performance Global Market Cache to prevent LiteDB bottlenecks during signal processing - public ConcurrentDictionary MarketCache { get; } = new(StringComparer.OrdinalIgnoreCase); - - // Master Trader Position Tracker: Tracks how many shares each master trader holds per token. - // Key: "{TraderId}_{TokenId}", Value: (Shares, LastUpdated) - // Used to determine if a SELL signal is a partial sell (ignore) or a full exit (copy). - public ConcurrentDictionary MasterTraderPositions { get; } = new(); - } -} diff --git a/frm_main.cs b/frm_main.cs index c6bfdeb..32b49c4 100644 --- a/frm_main.cs +++ b/frm_main.cs @@ -15,6 +15,7 @@ namespace PolyTraderSharp public partial class frm_main : Form { private readonly TradingState _tradingState; + private readonly CopyTradingState _copyState; private readonly TerminalLogger _logger; private readonly ThreemaService _threemaService; private readonly MullvadVpnService _vpnService; @@ -58,10 +59,11 @@ namespace PolyTraderSharp private DateTimePicker _dtpClosedTrades; private bool _terminalAutoScroll = true; - public frm_main(TradingState tradingState, TerminalLogger logger, ThreemaService threemaService, MullvadVpnService vpnService, IMongoDatabase db, PolymarketApiService api, PolymarketClobClient clob, JobManager jobManager) + public frm_main(TradingState tradingState, CopyTradingState copyState, TerminalLogger logger, ThreemaService threemaService, MullvadVpnService vpnService, IMongoDatabase db, PolymarketApiService api, PolymarketClobClient clob, JobManager jobManager) { InitializeComponent(); _tradingState = tradingState; + _copyState = copyState; _logger = logger; _threemaService = threemaService; _vpnService = vpnService; @@ -185,7 +187,7 @@ namespace PolyTraderSharp int clearedCount = 0; var validAccountIds = _tradingState.Accounts.Keys.ToList(); - foreach (var trader in _tradingState.Traders.Values) + foreach (var trader in _copyState.Traders.Values) { var orphanedIds = trader.AssignedAccountIds.Where(id => !validAccountIds.Contains(id)).ToList(); if (orphanedIds.Count > 0) @@ -436,13 +438,13 @@ namespace PolyTraderSharp btn_debugorderpayload.Checked = _tradingState.DebugOrderPayloadLog; }; - btn_sixshares.Checked = _tradingState.SixSharesMinimum; - btn_sixshares.BackColor = _tradingState.SixSharesMinimum ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed; + btn_sixshares.Checked = _copyState.SixSharesMinimum; + btn_sixshares.BackColor = _copyState.SixSharesMinimum ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed; btn_sixshares.Click += (s, ev) => { - _tradingState.SixSharesMinimum = !_tradingState.SixSharesMinimum; - btn_sixshares.Checked = _tradingState.SixSharesMinimum; - btn_sixshares.BackColor = _tradingState.SixSharesMinimum ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed; + _copyState.SixSharesMinimum = !_copyState.SixSharesMinimum; + btn_sixshares.Checked = _copyState.SixSharesMinimum; + btn_sixshares.BackColor = _copyState.SixSharesMinimum ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed; }; btn_debugMTHistory.Click += async (s, ev) => @@ -798,7 +800,7 @@ namespace PolyTraderSharp private void UpdateTraderAnalyticsUI(int accountId) { - if (_tradingState.TraderAnalyticsCache.TryGetValue(accountId, out var results) && results != null) + if (_copyState.TraderAnalyticsCache.TryGetValue(accountId, out var results) && results != null) { // Top Traders (Positive or 0 PnL, descending) var top = results.Where(x => x.Pnl30T >= 0).OrderByDescending(x => x.Pnl30T).ToList(); @@ -944,7 +946,7 @@ namespace PolyTraderSharp ExitReason = t.ExitReason }; r.AccountName = _tradingState.Accounts.TryGetValue(t.AccountId, out var a) ? a.Name : "Unknown"; - if (_tradingState.Traders.TryGetValue(t.SourceTraderId, out var mt)) + if (_copyState.Traders.TryGetValue(t.SourceTraderId, out var mt)) { r.SourceTraderName = mt.DisplayName; r.SourceTraderAddress = mt.WalletAddress; @@ -1027,9 +1029,9 @@ namespace PolyTraderSharp private void btn_Mastertraders_add_Click(object? sender, EventArgs e) { - int newId = _tradingState.Traders.Count > 0 ? _tradingState.Traders.Keys.Max() + 1 : 1; + int newId = _copyState.Traders.Count > 0 ? _copyState.Traders.Keys.Max() + 1 : 1; var newTrd = new TrackedTrader { Id = newId, DisplayName = "Neuer Master", WalletAddress = "0x..." }; - _tradingState.Traders[newTrd.Id] = newTrd; + _copyState.Traders[newTrd.Id] = newTrd; SaveTrader(newTrd); RefreshMasterGrid(); _bsMasters.Position = _bsMasters.Count - 1; @@ -1042,7 +1044,7 @@ namespace PolyTraderSharp var confirm = MessageBox.Show($"Möchten Sie den Master-Trader '{trd.DisplayName}' wirklich löschen?", "Löschen bestätigen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (confirm == DialogResult.Yes) { - _tradingState.Traders.TryRemove(trd.Id, out _); + _copyState.Traders.TryRemove(trd.Id, out _); _db.GetCollection("trackers").DeleteMany(x => x.Id == trd.Id); RefreshMasterGrid(); } @@ -1206,7 +1208,7 @@ namespace PolyTraderSharp private void RefreshMasterGrid() { var currentPos = _bsMasters.Position; - _bsMasters.DataSource = _tradingState.Traders.Values.ToList(); + _bsMasters.DataSource = _copyState.Traders.Values.ToList(); _bsMasters.ResetBindings(false); if (currentPos >= 0 && currentPos < _bsMasters.Count) _bsMasters.Position = currentPos; } @@ -1413,7 +1415,7 @@ namespace PolyTraderSharp var ct = new ClosedTrade { - TradeId = _tradingState.GetNextTradeId(), + TradeId = _copyState.GetNextTradeId(), AccountId = account.AccountId, IsDemo = true, MarketSlug = openPos.MarketSlug, @@ -1506,7 +1508,7 @@ namespace PolyTraderSharp var ct = new ClosedTrade { - TradeId = _tradingState.GetNextTradeId(), + TradeId = _copyState.GetNextTradeId(), AccountId = account.AccountId, IsDemo = false, MarketSlug = liveOpenPos.MarketSlug, diff --git a/services/AlchemyWebsocketService.cs b/services/AlchemyWebsocketService.cs index 109b47a..e6387eb 100644 --- a/services/AlchemyWebsocketService.cs +++ b/services/AlchemyWebsocketService.cs @@ -19,17 +19,20 @@ namespace PolyTraderSharp.Services private const string TransferBatchTopic = "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7ce"; private readonly TradingState _state; + private readonly CopyTradingState _copyState; private readonly ServerSettings _settings; private readonly TraderMonitorService _traderMonitor; private readonly TerminalLogger _logger; public AlchemyWebsocketService( TradingState state, + CopyTradingState copyState, ServerSettings settings, TraderMonitorService traderMonitor, TerminalLogger logger) { _state = state; + _copyState = copyState; _settings = settings; _traderMonitor = traderMonitor; _logger = logger; @@ -96,7 +99,7 @@ namespace PolyTraderSharp.Services _state.IsAlchemyHealthy = true; _logger.Info("✅ Alchemy WSS Connected. Dynamic API-Throttling activated."); - var activeTraders = _state.Traders.Values.Where(t => t.IsActive).ToList(); + var activeTraders = _copyState.Traders.Values.Where(t => t.IsActive).ToList(); var activeStateHash = string.Join(",", activeTraders.OrderBy(t => t.Id).Select(t => t.WalletAddress.ToLowerInvariant())); if (activeTraders.Count > 0) @@ -165,7 +168,7 @@ namespace PolyTraderSharp.Services while (!loopCts.IsCancellationRequested) { await Task.Delay(5000, loopCts.Token); - var currentTraders = _state.Traders.Values.Where(t => t.IsActive).ToList(); + var currentTraders = _copyState.Traders.Values.Where(t => t.IsActive).ToList(); var currentHash = string.Join(",", currentTraders.OrderBy(t => t.Id).Select(t => t.WalletAddress.ToLowerInvariant())); if (currentHash != activeStateHash) { @@ -223,7 +226,7 @@ namespace PolyTraderSharp.Services if (fromTopic == null || toTopic == null) return; - var activeTraders = _state.Traders.Values.Where(t => t.IsActive).ToList(); + var activeTraders = _copyState.Traders.Values.Where(t => t.IsActive).ToList(); string? triggeredAddress = null; foreach (var trader in activeTraders) diff --git a/services/CopyTradingEngine.cs b/services/CopyTradingEngine.cs index 2f05ec6..f338c75 100644 --- a/services/CopyTradingEngine.cs +++ b/services/CopyTradingEngine.cs @@ -16,6 +16,7 @@ namespace PolyTraderSharp.Services public class CopyTradingEngine : BackgroundService { private readonly TradingState _state; + private readonly CopyTradingState _copyState; private readonly ChannelReader _signalReader; private readonly ChannelWriter _closedTradeWriter; private readonly TerminalLogger _logger; @@ -29,6 +30,7 @@ namespace PolyTraderSharp.Services public CopyTradingEngine( TradingState state, + CopyTradingState copyState, ChannelReader signalReader, ChannelWriter closedTradeWriter, TerminalLogger logger, @@ -39,6 +41,7 @@ namespace PolyTraderSharp.Services IAccountRepository accountRepo) { _state = state; + _copyState = copyState; _signalReader = signalReader; _closedTradeWriter = closedTradeWriter; _logger = logger; @@ -185,7 +188,7 @@ namespace PolyTraderSharp.Services return; } - if (!_state.Traders.TryGetValue(signal.TraderId, out var trader) || !trader.IsActive) + if (!_copyState.Traders.TryGetValue(signal.TraderId, out var trader) || !trader.IsActive) { _logger.TradeReasoning($"\u23f8\ufe0f Trade {signal.Side} [{signal.MarketQuestion}] ignoriert: Trader (ID={signal.TraderId}) nicht gefunden oder inaktiv."); return; @@ -287,7 +290,7 @@ namespace PolyTraderSharp.Services if (account.TotalBalance < 150m) maxAllowed = Math.Min(1.20m, Math.Max(account.AvailableBalance, 0m)); else if (account.TotalBalance < 500m) maxAllowed = Math.Min(3.0m, Math.Max(account.AvailableBalance, 0m)); - if (_state.SixSharesMinimum && account.TotalBalance < 500m) + if (_copyState.SixSharesMinimum && account.TotalBalance < 500m) { // Adjust maxAllowed to cover at least 6 shares * order limit price. decimal desiredLimitForSix; @@ -459,7 +462,7 @@ namespace PolyTraderSharp.Services // Active day-traders like SwissTony buy 500 shares then sell 2 (0.4%) — we should NOT copy that. // But if they sell 200 of 500 (40%), that's a real exit signal we must copy. string masterPosKey = $"{signal.TraderId}_{inPortfolio.TokenId}"; - if (_state.MasterTraderPositions.TryGetValue(masterPosKey, out var masterPos)) + if (_copyState.MasterTraderPositions.TryGetValue(masterPosKey, out var masterPos)) { decimal masterShares = masterPos.Shares; if (masterShares > 0 && signal.Size > 0) @@ -524,7 +527,7 @@ namespace PolyTraderSharp.Services ExpiryDate = signal.EndDate ?? DateTime.UtcNow.AddDays(14) }; - _state.GetNextTradeId(); + _copyState.GetNextTradeId(); var finalPos = account.OpenPositions.AddOrUpdate(signal.TokenId, pos, (k, old) => { @@ -569,7 +572,7 @@ namespace PolyTraderSharp.Services ExpiryDate = signal.EndDate ?? DateTime.UtcNow.AddDays(14) }; - _state.GetNextTradeId(); + _copyState.GetNextTradeId(); account.OpenPositions.AddOrUpdate(signal.TokenId, pos, (k, old) => { old.Size += pos.Size; @@ -588,12 +591,12 @@ namespace PolyTraderSharp.Services // Track order placement time for stale order cleanup string orderKey = $"{account.AccountId}_{signal.TokenId}"; - _state.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow, signal.TraderId); + _copyState.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow, signal.TraderId); // Initialize master position tracking with signal size if not yet tracked // The background sync will update with the real value within 30 seconds string masterKey = $"{signal.TraderId}_{signal.TokenId}"; - _state.MasterTraderPositions.TryAdd(masterKey, (signal.Size, DateTime.UtcNow)); + _copyState.MasterTraderPositions.TryAdd(masterKey, (signal.Size, DateTime.UtcNow)); } } } @@ -601,7 +604,7 @@ namespace PolyTraderSharp.Services else if (signal.Side == "SELL") { string orderKey = $"{account.AccountId}_{signal.TokenId}"; - if (_state.PendingOrderTimestamps.TryGetValue(orderKey, out var pendingInfo)) + if (_copyState.PendingOrderTimestamps.TryGetValue(orderKey, out var pendingInfo)) { if ((DateTime.UtcNow - pendingInfo.PlacedAt).TotalSeconds < 20) { @@ -654,7 +657,7 @@ namespace PolyTraderSharp.Services var ct = new ClosedTrade { - TradeId = _state.GetNextTradeId(), + TradeId = _copyState.GetNextTradeId(), AccountId = account.AccountId, SourceTraderId = signal.TraderId, IsDemo = account.IsDemo, @@ -703,7 +706,7 @@ namespace PolyTraderSharp.Services if (result == "OK") { // Track order placement time for stale order cleanup / sync routines - _state.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow, signal.TraderId); + _copyState.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow, signal.TraderId); _logger.Trade($"✅ [LIVE MARKET SELL PLATZIERT] - {account.Name} - MARKET Swept. Gewinne/Verluste in Kürze im API Sync sichtbar."); } @@ -716,7 +719,7 @@ namespace PolyTraderSharp.Services $" Aktion: Position bleibt vorerst im Portfolio erhalten."); // Temporären Cooldown (5 Sek) setzen, um Log-Spam durch wiederholte API-Fehler zu vermeiden - _state.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow.AddSeconds(-15), signal.TraderId); + _copyState.PendingOrderTimestamps[orderKey] = (DateTime.UtcNow.AddSeconds(-15), signal.TraderId); // Reverse the TryRemove if it failed, so the next poll can try again account.OpenPositions.TryAdd(signal.TokenId, openPos); diff --git a/services/MasterTraderAnalyticsJob.cs b/services/MasterTraderAnalyticsJob.cs index 62860d4..dbaa4ca 100644 --- a/services/MasterTraderAnalyticsJob.cs +++ b/services/MasterTraderAnalyticsJob.cs @@ -14,14 +14,16 @@ namespace PolyTraderSharp.Services public class MasterTraderAnalyticsJob : BackgroundService { private readonly TradingState _state; + private readonly CopyTradingState _copyState; private readonly TerminalLogger _logger; private readonly IMongoDatabase _db; private readonly JobStatusRow _jobStatus; private readonly PolymarketApiService _api; - public MasterTraderAnalyticsJob(TradingState state, TerminalLogger logger, IMongoDatabase db, JobManager jobManager, PolymarketApiService api) + public MasterTraderAnalyticsJob(TradingState state, CopyTradingState copyState, TerminalLogger logger, IMongoDatabase db, JobManager jobManager, PolymarketApiService api) { _state = state; + _copyState = copyState; _logger = logger; _db = db; _api = api; @@ -90,7 +92,7 @@ namespace PolyTraderSharp.Services historyColl.EnsureIndex(x => x.ClosedAt); DateTime cutoff7Days = DateTime.UtcNow.AddDays(-7); - var tradersToAnalyze = _state.Traders.Values.Where(t => t.IsActive && !string.IsNullOrEmpty(t.WalletAddress)).ToList(); + var tradersToAnalyze = _copyState.Traders.Values.Where(t => t.IsActive && !string.IsNullOrEmpty(t.WalletAddress)).ToList(); foreach (var trader in tradersToAnalyze) { diff --git a/services/PolymarketWssClient.cs b/services/PolymarketWssClient.cs index eafec52..ff68cd0 100644 --- a/services/PolymarketWssClient.cs +++ b/services/PolymarketWssClient.cs @@ -20,6 +20,7 @@ namespace PolyTraderSharp.Services private const string MarketWssUrl = "wss://ws-subscriptions-clob.polymarket.com/ws/market"; private readonly TradingState _state; + private readonly CopyTradingState _copyState; private readonly ServerSettings _settings; private readonly PolymarketClobClient _clob; private readonly TerminalLogger _logger; @@ -32,6 +33,7 @@ namespace PolyTraderSharp.Services public PolymarketWssClient( TradingState state, + CopyTradingState copyState, ServerSettings settings, PolymarketClobClient clob, TerminalLogger logger, @@ -40,6 +42,7 @@ namespace PolyTraderSharp.Services IAccountRepository accountRepo) { _state = state; + _copyState = copyState; _settings = settings; _clob = clob; _logger = logger; @@ -266,7 +269,7 @@ namespace PolyTraderSharp.Services var ct = new ClosedTrade { - TradeId = _state.GetNextTradeId(), + TradeId = _copyState.GetNextTradeId(), AccountId = acc.AccountId, IsDemo = true, MarketSlug = pos.MarketSlug, diff --git a/services/SnapshotService.cs b/services/SnapshotService.cs index 28aab75..87a1d2d 100644 --- a/services/SnapshotService.cs +++ b/services/SnapshotService.cs @@ -8,15 +8,17 @@ 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, ILogger logger, JobManager jobManager) + public SnapshotService(TradingState state, CopyTradingState copyState, ILogger logger, JobManager jobManager) { _state = state; + _copyState = copyState; _logger = logger; _jobStatus = new JobStatusRow @@ -51,7 +53,7 @@ namespace PolyTraderSharp.Services { _state.LiveTradingMode = snapshot.LiveMode; _state.DemoTradingMode = snapshot.DemoMode; - _state.TotalCopyTrades = snapshot.CopyTrades; + _copyState.TotalCopyTrades = snapshot.CopyTrades; _state.GlobalPnl = snapshot.GlobalPnl; int restoredPositions = 0; @@ -124,7 +126,7 @@ namespace PolyTraderSharp.Services { LiveMode = _state.LiveTradingMode, DemoMode = _state.DemoTradingMode, - CopyTrades = _state.TotalCopyTrades, + CopyTrades = _copyState.TotalCopyTrades, GlobalPnl = _state.GlobalPnl, OpenPositions = _state.Accounts.ToDictionary( a => a.Key, diff --git a/services/StartupHydrationService.cs b/services/StartupHydrationService.cs index 5e6c2ba..b5e7512 100644 --- a/services/StartupHydrationService.cs +++ b/services/StartupHydrationService.cs @@ -18,6 +18,7 @@ namespace PolyTraderSharp.Services public class StartupHydrationService : IHostedService { private readonly TradingState _state; + private readonly CopyTradingState _copyState; private readonly IAccountRepository _accountRepo; private readonly IPositionRepository _positionRepo; private readonly IMongoDatabase _db; @@ -25,12 +26,14 @@ namespace PolyTraderSharp.Services public StartupHydrationService( TradingState state, + CopyTradingState copyState, IAccountRepository accountRepo, IPositionRepository positionRepo, IMongoDatabase db, TerminalLogger logger) { _state = state; + _copyState = copyState; _accountRepo = accountRepo; _positionRepo = positionRepo; _db = db; @@ -58,10 +61,10 @@ namespace PolyTraderSharp.Services var tradersCol = _db.GetCollection("trackers"); foreach (var trd in tradersCol.Find(_ => true).ToList()) { - _state.Traders[trd.Id] = trd; + _copyState.Traders[trd.Id] = trd; } - _logger.Info($"Startup-Hydration abgeschlossen: {_state.Accounts.Count} Accounts, {_state.Traders.Count} Trader geladen."); + _logger.Info($"Startup-Hydration abgeschlossen: {_state.Accounts.Count} Accounts, {_copyState.Traders.Count} Trader geladen."); } catch (System.Exception ex) { diff --git a/services/TraderAnalyticsJob.cs b/services/TraderAnalyticsJob.cs index bbfe809..1576119 100644 --- a/services/TraderAnalyticsJob.cs +++ b/services/TraderAnalyticsJob.cs @@ -13,13 +13,15 @@ namespace PolyTraderSharp.Services public class TraderAnalyticsJob : BackgroundService { private readonly TradingState _state; + private readonly CopyTradingState _copyState; private readonly TerminalLogger _logger; private readonly IMongoDatabase _db; private readonly JobStatusRow _jobStatus; - public TraderAnalyticsJob(TradingState state, TerminalLogger logger, IMongoDatabase db, JobManager jobManager) + public TraderAnalyticsJob(TradingState state, CopyTradingState copyState, TerminalLogger logger, IMongoDatabase db, JobManager jobManager) { _state = state; + _copyState = copyState; _logger = logger; _db = db; @@ -108,7 +110,7 @@ namespace PolyTraderSharp.Services foreach (var mtId in recentMTs) { - var mtInfo = _state.Traders.Values.FirstOrDefault(t => t.Id == mtId); + var mtInfo = _copyState.Traders.Values.FirstOrDefault(t => t.Id == mtId); string name = mtInfo?.DisplayName ?? $"MT #{mtId}"; string address = mtInfo?.WalletAddress ?? ""; @@ -141,7 +143,7 @@ namespace PolyTraderSharp.Services } // Save to cache - _state.TraderAnalyticsCache[acc.AccountId] = results; + _copyState.TraderAnalyticsCache[acc.AccountId] = results; } _logger.Info("✅ Trader Analytics erfolgreich abgeschlossen und im Cache aktualisiert."); diff --git a/services/TraderMonitorService.cs b/services/TraderMonitorService.cs index 9afc326..e2458ce 100644 --- a/services/TraderMonitorService.cs +++ b/services/TraderMonitorService.cs @@ -16,6 +16,7 @@ namespace PolyTraderSharp.Services public class TraderMonitorService : BackgroundService { private readonly TradingState _state; + private readonly CopyTradingState _copyState; private readonly PolymarketApiService _api; private readonly PolymarketClobClient _clob; private readonly ChannelWriter _signalWriter; @@ -39,8 +40,9 @@ namespace PolyTraderSharp.Services private static readonly ConcurrentDictionary _restRedeemAttempts = new(); public TraderMonitorService( - TradingState state, - PolymarketApiService api, + TradingState state, + CopyTradingState copyState, + PolymarketApiService api, PolymarketClobClient clob, ChannelWriter signalWriter, ChannelWriter closedTradeWriter, @@ -50,6 +52,7 @@ namespace PolyTraderSharp.Services IMongoDatabase? db = null) { _state = state; + _copyState = copyState; _api = api; _clob = clob; _signalWriter = signalWriter; @@ -102,7 +105,7 @@ namespace PolyTraderSharp.Services _lastLivePoll = DateTime.UtcNow; await SyncMasterTraderPositionsAsync(stoppingToken); _lastMasterPositionPoll = DateTime.UtcNow; - _logger.Info($"Startup: MasterTraderPositions warmup abgeschlossen ({_state.MasterTraderPositions.Count} Einträge)."); + _logger.Info($"Startup: MasterTraderPositions warmup abgeschlossen ({_copyState.MasterTraderPositions.Count} Einträge)."); } catch (Exception ex) { @@ -157,7 +160,7 @@ namespace PolyTraderSharp.Services return; } - var activeTraders = _state.Traders.Values.Where(t => t.IsActive).ToList(); + var activeTraders = _copyState.Traders.Values.Where(t => t.IsActive).ToList(); if (activeTraders.Count == 0) return; var now = DateTime.UtcNow; @@ -240,7 +243,7 @@ namespace PolyTraderSharp.Services public void TriggerFastBlockchainPoll(string txHash, string rpcUrl, string walletAddress) { - var trader = _state.Traders.Values.FirstOrDefault(t => t.WalletAddress.Equals(walletAddress, StringComparison.OrdinalIgnoreCase)); + var trader = _copyState.Traders.Values.FirstOrDefault(t => t.WalletAddress.Equals(walletAddress, StringComparison.OrdinalIgnoreCase)); if (trader == null || !trader.IsActive) return; if (_state.EnableBlockchainParser) @@ -391,7 +394,7 @@ namespace PolyTraderSharp.Services /// public void TriggerManualPoll(string walletAddress) { - var trader = _state.Traders.Values.FirstOrDefault(t => t.WalletAddress.Equals(walletAddress, StringComparison.OrdinalIgnoreCase)); + var trader = _copyState.Traders.Values.FirstOrDefault(t => t.WalletAddress.Equals(walletAddress, StringComparison.OrdinalIgnoreCase)); if (trader != null && trader.IsActive) { if (!_activeWssPolls.TryAdd(trader.WalletAddress, true)) @@ -625,12 +628,12 @@ namespace PolyTraderSharp.Services if (size > 0) exitPrice = (investment + realizedPnl) / size; string orderKey = $"{acc.AccountId}_{asset}"; - bool soldByUs = _state.PendingOrderTimestamps.ContainsKey(orderKey); + bool soldByUs = _copyState.PendingOrderTimestamps.ContainsKey(orderKey); string exitReason = soldByUs ? "Master Trader Sold" : "Manuell Geschlossen / System"; var ctRecord = new ClosedTrade { - TradeId = _state.GetNextTradeId(), + TradeId = _copyState.GetNextTradeId(), AccountId = acc.AccountId, SourceTraderId = resolvedSourceId, IsDemo = false, @@ -744,10 +747,10 @@ namespace PolyTraderSharp.Services // 1. Prüfe PendingOrderTimestamps (CopyTradingEngine hat diese Order kürzlich platziert) string orderKey = $"{acc.AccountId}_{asset}"; - if (_state.PendingOrderTimestamps.TryGetValue(orderKey, out var pending) && pending.SourceTraderId > 0) + if (_copyState.PendingOrderTimestamps.TryGetValue(orderKey, out var pending) && pending.SourceTraderId > 0) { resolvedTraderId = pending.SourceTraderId; - if (_state.Traders.TryGetValue(resolvedTraderId, out var pendingTrader)) + if (_copyState.Traders.TryGetValue(resolvedTraderId, out var pendingTrader)) { resolvedTraderName = pendingTrader.DisplayName; resolvedTraderAddress = pendingTrader.WalletAddress; @@ -920,12 +923,12 @@ namespace PolyTraderSharp.Services _logger.Info($"🏆 Live Market {removedPos.MarketQuestion} geschlossen! PnL: ${(realizedPnl):F2}"); string orderKey = $"{acc.AccountId}_{removedPos.TokenId}"; - bool soldByUs = _state.PendingOrderTimestamps.ContainsKey(orderKey); + bool soldByUs = _copyState.PendingOrderTimestamps.ContainsKey(orderKey); string exitReason = soldByUs ? "Master Trader Sold" : "Market Resolved"; var ctRecord = new ClosedTrade { - TradeId = _state.GetNextTradeId(), + TradeId = _copyState.GetNextTradeId(), AccountId = acc.AccountId, SourceTraderId = removedPos.SourceTraderId, IsDemo = false, @@ -943,7 +946,7 @@ namespace PolyTraderSharp.Services ExitReason = exitReason }; - if (soldByUs) _state.PendingOrderTimestamps.TryRemove(orderKey, out _); + if (soldByUs) _copyState.PendingOrderTimestamps.TryRemove(orderKey, out _); _processedClosures.TryAdd(duplicateKey, true); _closedTradeWriter.TryWrite(ctRecord); @@ -970,12 +973,12 @@ namespace PolyTraderSharp.Services _logger.Info($"🏆 Live Market {removedPos.MarketQuestion} aufgelöst (Fallback)! Auszahlung: ${(exitPrice * removedPos.Size):F2}"); string orderKey = $"{acc.AccountId}_{removedPos.TokenId}"; - bool soldByUs = _state.PendingOrderTimestamps.ContainsKey(orderKey); + bool soldByUs = _copyState.PendingOrderTimestamps.ContainsKey(orderKey); string exitReason = soldByUs ? "Master Trader Sold" : "Market Resolved"; var ctRecord = new ClosedTrade { - TradeId = _state.GetNextTradeId(), + TradeId = _copyState.GetNextTradeId(), AccountId = acc.AccountId, SourceTraderId = removedPos.SourceTraderId, IsDemo = false, @@ -993,7 +996,7 @@ namespace PolyTraderSharp.Services ExitReason = exitReason }; - if (soldByUs) _state.PendingOrderTimestamps.TryRemove(orderKey, out _); + if (soldByUs) _copyState.PendingOrderTimestamps.TryRemove(orderKey, out _); _processedClosures.TryAdd(duplicateKey, true); _closedTradeWriter.TryWrite(ctRecord); @@ -1079,7 +1082,7 @@ namespace PolyTraderSharp.Services { if (ct.IsCancellationRequested) break; - if (!_state.Traders.TryGetValue(traderId, out var trader) || string.IsNullOrEmpty(trader.WalletAddress)) + if (!_copyState.Traders.TryGetValue(traderId, out var trader) || string.IsNullOrEmpty(trader.WalletAddress)) continue; var positionSizes = await _api.GetTraderPositionSizesAsync(trader.WalletAddress, tokenIds); @@ -1089,7 +1092,7 @@ namespace PolyTraderSharp.Services { string key = $"{traderId}_{tokenId}"; decimal shares = positionSizes.ContainsKey(tokenId) ? positionSizes[tokenId] : 0m; - _state.MasterTraderPositions[key] = (shares, DateTime.UtcNow); + _copyState.MasterTraderPositions[key] = (shares, DateTime.UtcNow); } await Task.Delay(200, ct); // Brief delay between traders to avoid rate limits @@ -1104,7 +1107,7 @@ namespace PolyTraderSharp.Services private async Task CleanupStaleOpenOrdersAsync(CancellationToken ct) { - var keysToProcess = _state.PendingOrderTimestamps.ToArray(); + var keysToProcess = _copyState.PendingOrderTimestamps.ToArray(); if (keysToProcess.Length == 0) return; foreach (var kvp in keysToProcess) @@ -1119,7 +1122,7 @@ namespace PolyTraderSharp.Services // Determine timeout based on trader category int timeoutMinutes = 30; // Default: 30 min - if (_state.Traders.TryGetValue(kvp.Value.SourceTraderId, out var trader) && trader.Category == "HF") + if (_copyState.Traders.TryGetValue(kvp.Value.SourceTraderId, out var trader) && trader.Category == "HF") { timeoutMinutes = 3; // HF Trader: 3 min } @@ -1146,7 +1149,7 @@ namespace PolyTraderSharp.Services } // Remove from tracking regardless (even if cancel failed, we don't want to spam retries) - _state.PendingOrderTimestamps.TryRemove(kvp.Key, out _); + _copyState.PendingOrderTimestamps.TryRemove(kvp.Key, out _); } } diff --git a/src/PolyTrader.Core/TradingState.cs b/src/PolyTrader.Core/TradingState.cs new file mode 100644 index 0000000..0f8b522 --- /dev/null +++ b/src/PolyTrader.Core/TradingState.cs @@ -0,0 +1,39 @@ +using System.Collections.Concurrent; +using PolyTraderSharp.Models; + +namespace PolyTraderSharp +{ + public enum TradingMode + { + Inactive, + SellOnly, + Active + } + + /// + /// In-Memory Hot-Path State (Core-Teil). + /// Enthält den modulübergreifenden Zustand: eigene Accounts, globale Betriebsschalter, + /// Markt-Cache und Gesamt-PnL. Copytrading-spezifischer Zustand liegt im CopyTradingState + /// (siehe PolyTrader.Modules.CopyTrading). + /// + public class TradingState + { + // Globale Betriebsschalter + public bool GlobalTradingPaused { get; set; } = false; + public TradingMode LiveTradingMode { get; set; } = TradingMode.Inactive; + public TradingMode DemoTradingMode { get; set; } = TradingMode.Inactive; + public bool IsAlchemyHealthy { get; set; } = false; + public bool EnableBlockchainParser { get; set; } = true; + public bool DebugPollingLog { get; set; } = false; + public bool DebugOrderPayloadLog { get; set; } = false; + + // Eigene Trading-Accounts (AccountId -> State) + public ConcurrentDictionary Accounts { get; } = new(); + + // Aggregierte PnL über alle Module (Dashboard/Overview) + public decimal GlobalPnl { get; set; } = 0.0m; + + // High-Performance globaler Markt-Cache, verhindert DB-Flaschenhälse im Signalpfad. + public ConcurrentDictionary MarketCache { get; } = new(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/PolyTrader.Modules.CopyTrading/CopyTradingState.cs b/src/PolyTrader.Modules.CopyTrading/CopyTradingState.cs new file mode 100644 index 0000000..9d99769 --- /dev/null +++ b/src/PolyTrader.Modules.CopyTrading/CopyTradingState.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using PolyTraderSharp.Models; + +namespace PolyTraderSharp +{ + /// + /// In-Memory Hot-Path State des Copytrading-Moduls. + /// Enthält den copytrading-spezifischen Zustand: kopierte Master-Trader, deren + /// Positions-Tracking, Analytics-Cache, Copy-Trade-Zähler und Order-Timestamps. + /// Der modulübergreifende Zustand (Accounts, MarketCache, globale Schalter) liegt + /// im Core-. + /// + public class CopyTradingState + { + // Copytrading-Risk-Regel: mindestens 6 Shares pro Order erzwingen. + public bool SixSharesMinimum { get; set; } = true; + + // Kopierte Master-Trader (TraderId -> TrackedTrader) + public ConcurrentDictionary Traders { get; } = new(); + + private int _totalCopyTrades = 0; + public int TotalCopyTrades + { + get => _totalCopyTrades; + set => _totalCopyTrades = value; + } + + public int GetNextTradeId() + { + return Interlocked.Increment(ref _totalCopyTrades); + } + + // Analytics Cache (AccountId -> List) + public ConcurrentDictionary> TraderAnalyticsCache { get; } = new(); + + // Tracks when live orders were placed for stale order cleanup. + // Key: "AccountId_TokenId", Value: (PlacedAt, SourceTraderId) + public ConcurrentDictionary PendingOrderTimestamps { get; } = new(); + + // Master Trader Position Tracker (Key: "{TraderId}_{TokenId}", Value: (Shares, LastUpdated)) + // Entscheidet, ob ein SELL-Signal ein Teilverkauf (ignorieren) oder ein Voll-Exit (kopieren) ist. + public ConcurrentDictionary MasterTraderPositions { get; } = new(); + } +}