Phase 5.2: TradingState-Split (Core-State vs. CopyTradingState)

- 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>
This commit is contained in:
Richard
2026-07-01 20:09:50 +02:00
co-authored by Claude Opus 4.8
parent 55050a19e5
commit f8d395b2a3
13 changed files with 174 additions and 127 deletions
+14 -11
View File
@@ -16,6 +16,7 @@ namespace PolyTraderSharp.Services
public class CopyTradingEngine : BackgroundService
{
private readonly TradingState _state;
private readonly CopyTradingState _copyState;
private readonly ChannelReader<CopySignal> _signalReader;
private readonly ChannelWriter<ClosedTrade> _closedTradeWriter;
private readonly TerminalLogger _logger;
@@ -29,6 +30,7 @@ namespace PolyTraderSharp.Services
public CopyTradingEngine(
TradingState state,
CopyTradingState copyState,
ChannelReader<CopySignal> signalReader,
ChannelWriter<ClosedTrade> 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);