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:
co-authored by
Claude Opus 4.8
parent
55050a19e5
commit
f8d395b2a3
+4
-3
@@ -37,6 +37,7 @@ internal static class Program
|
|||||||
services.AddCorePersistence();
|
services.AddCorePersistence();
|
||||||
services.AddSingleton((IServiceProvider sp) => ServerSettings.Load("server_settings.xml"));
|
services.AddSingleton((IServiceProvider sp) => ServerSettings.Load("server_settings.xml"));
|
||||||
services.AddSingleton<TradingState>();
|
services.AddSingleton<TradingState>();
|
||||||
|
services.AddSingleton<CopyTradingState>();
|
||||||
services.AddSingleton(copySignalChannel.Writer);
|
services.AddSingleton(copySignalChannel.Writer);
|
||||||
services.AddSingleton(copySignalChannel.Reader);
|
services.AddSingleton(copySignalChannel.Reader);
|
||||||
services.AddSingleton(closedTradeChannel.Writer);
|
services.AddSingleton(closedTradeChannel.Writer);
|
||||||
@@ -102,16 +103,16 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
var state = AppHost.Services.GetRequiredService<TradingState>();
|
var copyState = AppHost.Services.GetRequiredService<CopyTradingState>();
|
||||||
var maxTradeDoc = db.GetCollection<MongoDB.Bson.BsonDocument>("closed_trades")
|
var maxTradeDoc = db.GetCollection<MongoDB.Bson.BsonDocument>("closed_trades")
|
||||||
.Find(Builders<MongoDB.Bson.BsonDocument>.Filter.Empty)
|
.Find(Builders<MongoDB.Bson.BsonDocument>.Filter.Empty)
|
||||||
.SortByDescending(d => d["_id"])
|
.SortByDescending(d => d["_id"])
|
||||||
.Limit(1)
|
.Limit(1)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (maxTradeDoc != null && maxTradeDoc.Contains("_id"))
|
if (maxTradeDoc != null && maxTradeDoc.Contains("_id"))
|
||||||
{
|
{
|
||||||
state.TotalCopyTrades = maxTradeDoc["_id"].AsInt32;
|
copyState.TotalCopyTrades = maxTradeDoc["_id"].AsInt32;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using PolyTraderSharp.Models;
|
|
||||||
|
|
||||||
namespace PolyTraderSharp
|
|
||||||
{
|
|
||||||
public enum TradingMode
|
|
||||||
{
|
|
||||||
Inactive,
|
|
||||||
SellOnly,
|
|
||||||
Active
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// In-Memory Hot-Path State for PolyTrader.
|
|
||||||
/// Replaces database lookups for core trading logic.
|
|
||||||
/// </summary>
|
|
||||||
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<int, AccountState> Accounts { get; } = new();
|
|
||||||
|
|
||||||
// Tracked Traders (TraderId -> TrackedTrader)
|
|
||||||
public ConcurrentDictionary<int, TrackedTrader> 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<TraderAnalyticsResult>)
|
|
||||||
public ConcurrentDictionary<int, List<TraderAnalyticsResult>> TraderAnalyticsCache { get; } = new();
|
|
||||||
|
|
||||||
// Tracks when live orders were placed for stale order cleanup
|
|
||||||
// Key: "AccountId_TokenId", Value: (PlacedAt, SourceTraderId)
|
|
||||||
public ConcurrentDictionary<string, (DateTime PlacedAt, int SourceTraderId)> PendingOrderTimestamps { get; } = new();
|
|
||||||
|
|
||||||
// High-Performance Global Market Cache to prevent LiteDB bottlenecks during signal processing
|
|
||||||
public ConcurrentDictionary<string, MarketData> 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<string, (decimal Shares, DateTime LastUpdated)> MasterTraderPositions { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+17
-15
@@ -15,6 +15,7 @@ namespace PolyTraderSharp
|
|||||||
public partial class frm_main : Form
|
public partial class frm_main : Form
|
||||||
{
|
{
|
||||||
private readonly TradingState _tradingState;
|
private readonly TradingState _tradingState;
|
||||||
|
private readonly CopyTradingState _copyState;
|
||||||
private readonly TerminalLogger _logger;
|
private readonly TerminalLogger _logger;
|
||||||
private readonly ThreemaService _threemaService;
|
private readonly ThreemaService _threemaService;
|
||||||
private readonly MullvadVpnService _vpnService;
|
private readonly MullvadVpnService _vpnService;
|
||||||
@@ -58,10 +59,11 @@ namespace PolyTraderSharp
|
|||||||
private DateTimePicker _dtpClosedTrades;
|
private DateTimePicker _dtpClosedTrades;
|
||||||
private bool _terminalAutoScroll = true;
|
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();
|
InitializeComponent();
|
||||||
_tradingState = tradingState;
|
_tradingState = tradingState;
|
||||||
|
_copyState = copyState;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_threemaService = threemaService;
|
_threemaService = threemaService;
|
||||||
_vpnService = vpnService;
|
_vpnService = vpnService;
|
||||||
@@ -185,7 +187,7 @@ namespace PolyTraderSharp
|
|||||||
int clearedCount = 0;
|
int clearedCount = 0;
|
||||||
var validAccountIds = _tradingState.Accounts.Keys.ToList();
|
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();
|
var orphanedIds = trader.AssignedAccountIds.Where(id => !validAccountIds.Contains(id)).ToList();
|
||||||
if (orphanedIds.Count > 0)
|
if (orphanedIds.Count > 0)
|
||||||
@@ -436,13 +438,13 @@ namespace PolyTraderSharp
|
|||||||
btn_debugorderpayload.Checked = _tradingState.DebugOrderPayloadLog;
|
btn_debugorderpayload.Checked = _tradingState.DebugOrderPayloadLog;
|
||||||
};
|
};
|
||||||
|
|
||||||
btn_sixshares.Checked = _tradingState.SixSharesMinimum;
|
btn_sixshares.Checked = _copyState.SixSharesMinimum;
|
||||||
btn_sixshares.BackColor = _tradingState.SixSharesMinimum ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed;
|
btn_sixshares.BackColor = _copyState.SixSharesMinimum ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed;
|
||||||
btn_sixshares.Click += (s, ev) =>
|
btn_sixshares.Click += (s, ev) =>
|
||||||
{
|
{
|
||||||
_tradingState.SixSharesMinimum = !_tradingState.SixSharesMinimum;
|
_copyState.SixSharesMinimum = !_copyState.SixSharesMinimum;
|
||||||
btn_sixshares.Checked = _tradingState.SixSharesMinimum;
|
btn_sixshares.Checked = _copyState.SixSharesMinimum;
|
||||||
btn_sixshares.BackColor = _tradingState.SixSharesMinimum ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed;
|
btn_sixshares.BackColor = _copyState.SixSharesMinimum ? System.Drawing.Color.LightGreen : System.Drawing.Color.IndianRed;
|
||||||
};
|
};
|
||||||
|
|
||||||
btn_debugMTHistory.Click += async (s, ev) =>
|
btn_debugMTHistory.Click += async (s, ev) =>
|
||||||
@@ -798,7 +800,7 @@ namespace PolyTraderSharp
|
|||||||
|
|
||||||
private void UpdateTraderAnalyticsUI(int accountId)
|
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)
|
// Top Traders (Positive or 0 PnL, descending)
|
||||||
var top = results.Where(x => x.Pnl30T >= 0).OrderByDescending(x => x.Pnl30T).ToList();
|
var top = results.Where(x => x.Pnl30T >= 0).OrderByDescending(x => x.Pnl30T).ToList();
|
||||||
@@ -944,7 +946,7 @@ namespace PolyTraderSharp
|
|||||||
ExitReason = t.ExitReason
|
ExitReason = t.ExitReason
|
||||||
};
|
};
|
||||||
r.AccountName = _tradingState.Accounts.TryGetValue(t.AccountId, out var a) ? a.Name : "Unknown";
|
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.SourceTraderName = mt.DisplayName;
|
||||||
r.SourceTraderAddress = mt.WalletAddress;
|
r.SourceTraderAddress = mt.WalletAddress;
|
||||||
@@ -1027,9 +1029,9 @@ namespace PolyTraderSharp
|
|||||||
|
|
||||||
private void btn_Mastertraders_add_Click(object? sender, EventArgs e)
|
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..." };
|
var newTrd = new TrackedTrader { Id = newId, DisplayName = "Neuer Master", WalletAddress = "0x..." };
|
||||||
_tradingState.Traders[newTrd.Id] = newTrd;
|
_copyState.Traders[newTrd.Id] = newTrd;
|
||||||
SaveTrader(newTrd);
|
SaveTrader(newTrd);
|
||||||
RefreshMasterGrid();
|
RefreshMasterGrid();
|
||||||
_bsMasters.Position = _bsMasters.Count - 1;
|
_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);
|
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)
|
if (confirm == DialogResult.Yes)
|
||||||
{
|
{
|
||||||
_tradingState.Traders.TryRemove(trd.Id, out _);
|
_copyState.Traders.TryRemove(trd.Id, out _);
|
||||||
_db.GetCollection<TrackedTrader>("trackers").DeleteMany(x => x.Id == trd.Id);
|
_db.GetCollection<TrackedTrader>("trackers").DeleteMany(x => x.Id == trd.Id);
|
||||||
RefreshMasterGrid();
|
RefreshMasterGrid();
|
||||||
}
|
}
|
||||||
@@ -1206,7 +1208,7 @@ namespace PolyTraderSharp
|
|||||||
private void RefreshMasterGrid()
|
private void RefreshMasterGrid()
|
||||||
{
|
{
|
||||||
var currentPos = _bsMasters.Position;
|
var currentPos = _bsMasters.Position;
|
||||||
_bsMasters.DataSource = _tradingState.Traders.Values.ToList();
|
_bsMasters.DataSource = _copyState.Traders.Values.ToList();
|
||||||
_bsMasters.ResetBindings(false);
|
_bsMasters.ResetBindings(false);
|
||||||
if (currentPos >= 0 && currentPos < _bsMasters.Count) _bsMasters.Position = currentPos;
|
if (currentPos >= 0 && currentPos < _bsMasters.Count) _bsMasters.Position = currentPos;
|
||||||
}
|
}
|
||||||
@@ -1413,7 +1415,7 @@ namespace PolyTraderSharp
|
|||||||
|
|
||||||
var ct = new ClosedTrade
|
var ct = new ClosedTrade
|
||||||
{
|
{
|
||||||
TradeId = _tradingState.GetNextTradeId(),
|
TradeId = _copyState.GetNextTradeId(),
|
||||||
AccountId = account.AccountId,
|
AccountId = account.AccountId,
|
||||||
IsDemo = true,
|
IsDemo = true,
|
||||||
MarketSlug = openPos.MarketSlug,
|
MarketSlug = openPos.MarketSlug,
|
||||||
@@ -1506,7 +1508,7 @@ namespace PolyTraderSharp
|
|||||||
|
|
||||||
var ct = new ClosedTrade
|
var ct = new ClosedTrade
|
||||||
{
|
{
|
||||||
TradeId = _tradingState.GetNextTradeId(),
|
TradeId = _copyState.GetNextTradeId(),
|
||||||
AccountId = account.AccountId,
|
AccountId = account.AccountId,
|
||||||
IsDemo = false,
|
IsDemo = false,
|
||||||
MarketSlug = liveOpenPos.MarketSlug,
|
MarketSlug = liveOpenPos.MarketSlug,
|
||||||
|
|||||||
@@ -19,17 +19,20 @@ namespace PolyTraderSharp.Services
|
|||||||
private const string TransferBatchTopic = "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7ce";
|
private const string TransferBatchTopic = "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7ce";
|
||||||
|
|
||||||
private readonly TradingState _state;
|
private readonly TradingState _state;
|
||||||
|
private readonly CopyTradingState _copyState;
|
||||||
private readonly ServerSettings _settings;
|
private readonly ServerSettings _settings;
|
||||||
private readonly TraderMonitorService _traderMonitor;
|
private readonly TraderMonitorService _traderMonitor;
|
||||||
private readonly TerminalLogger _logger;
|
private readonly TerminalLogger _logger;
|
||||||
|
|
||||||
public AlchemyWebsocketService(
|
public AlchemyWebsocketService(
|
||||||
TradingState state,
|
TradingState state,
|
||||||
|
CopyTradingState copyState,
|
||||||
ServerSettings settings,
|
ServerSettings settings,
|
||||||
TraderMonitorService traderMonitor,
|
TraderMonitorService traderMonitor,
|
||||||
TerminalLogger logger)
|
TerminalLogger logger)
|
||||||
{
|
{
|
||||||
_state = state;
|
_state = state;
|
||||||
|
_copyState = copyState;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_traderMonitor = traderMonitor;
|
_traderMonitor = traderMonitor;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -96,7 +99,7 @@ namespace PolyTraderSharp.Services
|
|||||||
_state.IsAlchemyHealthy = true;
|
_state.IsAlchemyHealthy = true;
|
||||||
_logger.Info("✅ Alchemy WSS Connected. Dynamic API-Throttling activated.");
|
_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()));
|
var activeStateHash = string.Join(",", activeTraders.OrderBy(t => t.Id).Select(t => t.WalletAddress.ToLowerInvariant()));
|
||||||
|
|
||||||
if (activeTraders.Count > 0)
|
if (activeTraders.Count > 0)
|
||||||
@@ -165,7 +168,7 @@ namespace PolyTraderSharp.Services
|
|||||||
while (!loopCts.IsCancellationRequested)
|
while (!loopCts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await Task.Delay(5000, loopCts.Token);
|
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()));
|
var currentHash = string.Join(",", currentTraders.OrderBy(t => t.Id).Select(t => t.WalletAddress.ToLowerInvariant()));
|
||||||
if (currentHash != activeStateHash)
|
if (currentHash != activeStateHash)
|
||||||
{
|
{
|
||||||
@@ -223,7 +226,7 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
if (fromTopic == null || toTopic == null) return;
|
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;
|
string? triggeredAddress = null;
|
||||||
|
|
||||||
foreach (var trader in activeTraders)
|
foreach (var trader in activeTraders)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace PolyTraderSharp.Services
|
|||||||
public class CopyTradingEngine : BackgroundService
|
public class CopyTradingEngine : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly TradingState _state;
|
private readonly TradingState _state;
|
||||||
|
private readonly CopyTradingState _copyState;
|
||||||
private readonly ChannelReader<CopySignal> _signalReader;
|
private readonly ChannelReader<CopySignal> _signalReader;
|
||||||
private readonly ChannelWriter<ClosedTrade> _closedTradeWriter;
|
private readonly ChannelWriter<ClosedTrade> _closedTradeWriter;
|
||||||
private readonly TerminalLogger _logger;
|
private readonly TerminalLogger _logger;
|
||||||
@@ -29,6 +30,7 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
public CopyTradingEngine(
|
public CopyTradingEngine(
|
||||||
TradingState state,
|
TradingState state,
|
||||||
|
CopyTradingState copyState,
|
||||||
ChannelReader<CopySignal> signalReader,
|
ChannelReader<CopySignal> signalReader,
|
||||||
ChannelWriter<ClosedTrade> closedTradeWriter,
|
ChannelWriter<ClosedTrade> closedTradeWriter,
|
||||||
TerminalLogger logger,
|
TerminalLogger logger,
|
||||||
@@ -39,6 +41,7 @@ namespace PolyTraderSharp.Services
|
|||||||
IAccountRepository accountRepo)
|
IAccountRepository accountRepo)
|
||||||
{
|
{
|
||||||
_state = state;
|
_state = state;
|
||||||
|
_copyState = copyState;
|
||||||
_signalReader = signalReader;
|
_signalReader = signalReader;
|
||||||
_closedTradeWriter = closedTradeWriter;
|
_closedTradeWriter = closedTradeWriter;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -185,7 +188,7 @@ namespace PolyTraderSharp.Services
|
|||||||
return;
|
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.");
|
_logger.TradeReasoning($"\u23f8\ufe0f Trade {signal.Side} [{signal.MarketQuestion}] ignoriert: Trader (ID={signal.TraderId}) nicht gefunden oder inaktiv.");
|
||||||
return;
|
return;
|
||||||
@@ -287,7 +290,7 @@ namespace PolyTraderSharp.Services
|
|||||||
if (account.TotalBalance < 150m) maxAllowed = Math.Min(1.20m, Math.Max(account.AvailableBalance, 0m));
|
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));
|
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.
|
// Adjust maxAllowed to cover at least 6 shares * order limit price.
|
||||||
decimal desiredLimitForSix;
|
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.
|
// 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.
|
// But if they sell 200 of 500 (40%), that's a real exit signal we must copy.
|
||||||
string masterPosKey = $"{signal.TraderId}_{inPortfolio.TokenId}";
|
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;
|
decimal masterShares = masterPos.Shares;
|
||||||
if (masterShares > 0 && signal.Size > 0)
|
if (masterShares > 0 && signal.Size > 0)
|
||||||
@@ -524,7 +527,7 @@ namespace PolyTraderSharp.Services
|
|||||||
ExpiryDate = signal.EndDate ?? DateTime.UtcNow.AddDays(14)
|
ExpiryDate = signal.EndDate ?? DateTime.UtcNow.AddDays(14)
|
||||||
};
|
};
|
||||||
|
|
||||||
_state.GetNextTradeId();
|
_copyState.GetNextTradeId();
|
||||||
|
|
||||||
var finalPos = account.OpenPositions.AddOrUpdate(signal.TokenId, pos, (k, old) =>
|
var finalPos = account.OpenPositions.AddOrUpdate(signal.TokenId, pos, (k, old) =>
|
||||||
{
|
{
|
||||||
@@ -569,7 +572,7 @@ namespace PolyTraderSharp.Services
|
|||||||
ExpiryDate = signal.EndDate ?? DateTime.UtcNow.AddDays(14)
|
ExpiryDate = signal.EndDate ?? DateTime.UtcNow.AddDays(14)
|
||||||
};
|
};
|
||||||
|
|
||||||
_state.GetNextTradeId();
|
_copyState.GetNextTradeId();
|
||||||
account.OpenPositions.AddOrUpdate(signal.TokenId, pos, (k, old) =>
|
account.OpenPositions.AddOrUpdate(signal.TokenId, pos, (k, old) =>
|
||||||
{
|
{
|
||||||
old.Size += pos.Size;
|
old.Size += pos.Size;
|
||||||
@@ -588,12 +591,12 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
// Track order placement time for stale order cleanup
|
// Track order placement time for stale order cleanup
|
||||||
string orderKey = $"{account.AccountId}_{signal.TokenId}";
|
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
|
// Initialize master position tracking with signal size if not yet tracked
|
||||||
// The background sync will update with the real value within 30 seconds
|
// The background sync will update with the real value within 30 seconds
|
||||||
string masterKey = $"{signal.TraderId}_{signal.TokenId}";
|
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")
|
else if (signal.Side == "SELL")
|
||||||
{
|
{
|
||||||
string orderKey = $"{account.AccountId}_{signal.TokenId}";
|
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)
|
if ((DateTime.UtcNow - pendingInfo.PlacedAt).TotalSeconds < 20)
|
||||||
{
|
{
|
||||||
@@ -654,7 +657,7 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
var ct = new ClosedTrade
|
var ct = new ClosedTrade
|
||||||
{
|
{
|
||||||
TradeId = _state.GetNextTradeId(),
|
TradeId = _copyState.GetNextTradeId(),
|
||||||
AccountId = account.AccountId,
|
AccountId = account.AccountId,
|
||||||
SourceTraderId = signal.TraderId,
|
SourceTraderId = signal.TraderId,
|
||||||
IsDemo = account.IsDemo,
|
IsDemo = account.IsDemo,
|
||||||
@@ -703,7 +706,7 @@ namespace PolyTraderSharp.Services
|
|||||||
if (result == "OK")
|
if (result == "OK")
|
||||||
{
|
{
|
||||||
// Track order placement time for stale order cleanup / sync routines
|
// 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.");
|
_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.");
|
$" Aktion: Position bleibt vorerst im Portfolio erhalten.");
|
||||||
|
|
||||||
// Temporären Cooldown (5 Sek) setzen, um Log-Spam durch wiederholte API-Fehler zu vermeiden
|
// 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
|
// Reverse the TryRemove if it failed, so the next poll can try again
|
||||||
account.OpenPositions.TryAdd(signal.TokenId, openPos);
|
account.OpenPositions.TryAdd(signal.TokenId, openPos);
|
||||||
|
|||||||
@@ -14,14 +14,16 @@ namespace PolyTraderSharp.Services
|
|||||||
public class MasterTraderAnalyticsJob : BackgroundService
|
public class MasterTraderAnalyticsJob : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly TradingState _state;
|
private readonly TradingState _state;
|
||||||
|
private readonly CopyTradingState _copyState;
|
||||||
private readonly TerminalLogger _logger;
|
private readonly TerminalLogger _logger;
|
||||||
private readonly IMongoDatabase _db;
|
private readonly IMongoDatabase _db;
|
||||||
private readonly JobStatusRow _jobStatus;
|
private readonly JobStatusRow _jobStatus;
|
||||||
private readonly PolymarketApiService _api;
|
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;
|
_state = state;
|
||||||
|
_copyState = copyState;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_db = db;
|
_db = db;
|
||||||
_api = api;
|
_api = api;
|
||||||
@@ -90,7 +92,7 @@ namespace PolyTraderSharp.Services
|
|||||||
historyColl.EnsureIndex(x => x.ClosedAt);
|
historyColl.EnsureIndex(x => x.ClosedAt);
|
||||||
|
|
||||||
DateTime cutoff7Days = DateTime.UtcNow.AddDays(-7);
|
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)
|
foreach (var trader in tradersToAnalyze)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace PolyTraderSharp.Services
|
|||||||
private const string MarketWssUrl = "wss://ws-subscriptions-clob.polymarket.com/ws/market";
|
private const string MarketWssUrl = "wss://ws-subscriptions-clob.polymarket.com/ws/market";
|
||||||
|
|
||||||
private readonly TradingState _state;
|
private readonly TradingState _state;
|
||||||
|
private readonly CopyTradingState _copyState;
|
||||||
private readonly ServerSettings _settings;
|
private readonly ServerSettings _settings;
|
||||||
private readonly PolymarketClobClient _clob;
|
private readonly PolymarketClobClient _clob;
|
||||||
private readonly TerminalLogger _logger;
|
private readonly TerminalLogger _logger;
|
||||||
@@ -32,6 +33,7 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
public PolymarketWssClient(
|
public PolymarketWssClient(
|
||||||
TradingState state,
|
TradingState state,
|
||||||
|
CopyTradingState copyState,
|
||||||
ServerSettings settings,
|
ServerSettings settings,
|
||||||
PolymarketClobClient clob,
|
PolymarketClobClient clob,
|
||||||
TerminalLogger logger,
|
TerminalLogger logger,
|
||||||
@@ -40,6 +42,7 @@ namespace PolyTraderSharp.Services
|
|||||||
IAccountRepository accountRepo)
|
IAccountRepository accountRepo)
|
||||||
{
|
{
|
||||||
_state = state;
|
_state = state;
|
||||||
|
_copyState = copyState;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_clob = clob;
|
_clob = clob;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -266,7 +269,7 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
var ct = new ClosedTrade
|
var ct = new ClosedTrade
|
||||||
{
|
{
|
||||||
TradeId = _state.GetNextTradeId(),
|
TradeId = _copyState.GetNextTradeId(),
|
||||||
AccountId = acc.AccountId,
|
AccountId = acc.AccountId,
|
||||||
IsDemo = true,
|
IsDemo = true,
|
||||||
MarketSlug = pos.MarketSlug,
|
MarketSlug = pos.MarketSlug,
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ namespace PolyTraderSharp.Services
|
|||||||
public class SnapshotService : BackgroundService
|
public class SnapshotService : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly TradingState _state;
|
private readonly TradingState _state;
|
||||||
|
private readonly CopyTradingState _copyState;
|
||||||
private readonly ILogger<SnapshotService> _logger;
|
private readonly ILogger<SnapshotService> _logger;
|
||||||
private readonly string _snapshotPath = "snapshot.json";
|
private readonly string _snapshotPath = "snapshot.json";
|
||||||
private readonly TimeSpan _interval = TimeSpan.FromSeconds(30);
|
private readonly TimeSpan _interval = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
private readonly JobStatusRow _jobStatus;
|
private readonly JobStatusRow _jobStatus;
|
||||||
|
|
||||||
public SnapshotService(TradingState state, ILogger<SnapshotService> logger, JobManager jobManager)
|
public SnapshotService(TradingState state, CopyTradingState copyState, ILogger<SnapshotService> logger, JobManager jobManager)
|
||||||
{
|
{
|
||||||
_state = state;
|
_state = state;
|
||||||
|
_copyState = copyState;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
_jobStatus = new JobStatusRow
|
_jobStatus = new JobStatusRow
|
||||||
@@ -51,7 +53,7 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
_state.LiveTradingMode = snapshot.LiveMode;
|
_state.LiveTradingMode = snapshot.LiveMode;
|
||||||
_state.DemoTradingMode = snapshot.DemoMode;
|
_state.DemoTradingMode = snapshot.DemoMode;
|
||||||
_state.TotalCopyTrades = snapshot.CopyTrades;
|
_copyState.TotalCopyTrades = snapshot.CopyTrades;
|
||||||
_state.GlobalPnl = snapshot.GlobalPnl;
|
_state.GlobalPnl = snapshot.GlobalPnl;
|
||||||
|
|
||||||
int restoredPositions = 0;
|
int restoredPositions = 0;
|
||||||
@@ -124,7 +126,7 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
LiveMode = _state.LiveTradingMode,
|
LiveMode = _state.LiveTradingMode,
|
||||||
DemoMode = _state.DemoTradingMode,
|
DemoMode = _state.DemoTradingMode,
|
||||||
CopyTrades = _state.TotalCopyTrades,
|
CopyTrades = _copyState.TotalCopyTrades,
|
||||||
GlobalPnl = _state.GlobalPnl,
|
GlobalPnl = _state.GlobalPnl,
|
||||||
OpenPositions = _state.Accounts.ToDictionary(
|
OpenPositions = _state.Accounts.ToDictionary(
|
||||||
a => a.Key,
|
a => a.Key,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace PolyTraderSharp.Services
|
|||||||
public class StartupHydrationService : IHostedService
|
public class StartupHydrationService : IHostedService
|
||||||
{
|
{
|
||||||
private readonly TradingState _state;
|
private readonly TradingState _state;
|
||||||
|
private readonly CopyTradingState _copyState;
|
||||||
private readonly IAccountRepository _accountRepo;
|
private readonly IAccountRepository _accountRepo;
|
||||||
private readonly IPositionRepository _positionRepo;
|
private readonly IPositionRepository _positionRepo;
|
||||||
private readonly IMongoDatabase _db;
|
private readonly IMongoDatabase _db;
|
||||||
@@ -25,12 +26,14 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
public StartupHydrationService(
|
public StartupHydrationService(
|
||||||
TradingState state,
|
TradingState state,
|
||||||
|
CopyTradingState copyState,
|
||||||
IAccountRepository accountRepo,
|
IAccountRepository accountRepo,
|
||||||
IPositionRepository positionRepo,
|
IPositionRepository positionRepo,
|
||||||
IMongoDatabase db,
|
IMongoDatabase db,
|
||||||
TerminalLogger logger)
|
TerminalLogger logger)
|
||||||
{
|
{
|
||||||
_state = state;
|
_state = state;
|
||||||
|
_copyState = copyState;
|
||||||
_accountRepo = accountRepo;
|
_accountRepo = accountRepo;
|
||||||
_positionRepo = positionRepo;
|
_positionRepo = positionRepo;
|
||||||
_db = db;
|
_db = db;
|
||||||
@@ -58,10 +61,10 @@ namespace PolyTraderSharp.Services
|
|||||||
var tradersCol = _db.GetCollection<TrackedTrader>("trackers");
|
var tradersCol = _db.GetCollection<TrackedTrader>("trackers");
|
||||||
foreach (var trd in tradersCol.Find(_ => true).ToList())
|
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)
|
catch (System.Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,13 +13,15 @@ namespace PolyTraderSharp.Services
|
|||||||
public class TraderAnalyticsJob : BackgroundService
|
public class TraderAnalyticsJob : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly TradingState _state;
|
private readonly TradingState _state;
|
||||||
|
private readonly CopyTradingState _copyState;
|
||||||
private readonly TerminalLogger _logger;
|
private readonly TerminalLogger _logger;
|
||||||
private readonly IMongoDatabase _db;
|
private readonly IMongoDatabase _db;
|
||||||
private readonly JobStatusRow _jobStatus;
|
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;
|
_state = state;
|
||||||
|
_copyState = copyState;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_db = db;
|
_db = db;
|
||||||
|
|
||||||
@@ -108,7 +110,7 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
foreach (var mtId in recentMTs)
|
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 name = mtInfo?.DisplayName ?? $"MT #{mtId}";
|
||||||
string address = mtInfo?.WalletAddress ?? "";
|
string address = mtInfo?.WalletAddress ?? "";
|
||||||
|
|
||||||
@@ -141,7 +143,7 @@ namespace PolyTraderSharp.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save to cache
|
// Save to cache
|
||||||
_state.TraderAnalyticsCache[acc.AccountId] = results;
|
_copyState.TraderAnalyticsCache[acc.AccountId] = results;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Info("✅ Trader Analytics erfolgreich abgeschlossen und im Cache aktualisiert.");
|
_logger.Info("✅ Trader Analytics erfolgreich abgeschlossen und im Cache aktualisiert.");
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace PolyTraderSharp.Services
|
|||||||
public class TraderMonitorService : BackgroundService
|
public class TraderMonitorService : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly TradingState _state;
|
private readonly TradingState _state;
|
||||||
|
private readonly CopyTradingState _copyState;
|
||||||
private readonly PolymarketApiService _api;
|
private readonly PolymarketApiService _api;
|
||||||
private readonly PolymarketClobClient _clob;
|
private readonly PolymarketClobClient _clob;
|
||||||
private readonly ChannelWriter<CopySignal> _signalWriter;
|
private readonly ChannelWriter<CopySignal> _signalWriter;
|
||||||
@@ -39,8 +40,9 @@ namespace PolyTraderSharp.Services
|
|||||||
private static readonly ConcurrentDictionary<string, (int Count, DateTime LastAttempt)> _restRedeemAttempts = new();
|
private static readonly ConcurrentDictionary<string, (int Count, DateTime LastAttempt)> _restRedeemAttempts = new();
|
||||||
|
|
||||||
public TraderMonitorService(
|
public TraderMonitorService(
|
||||||
TradingState state,
|
TradingState state,
|
||||||
PolymarketApiService api,
|
CopyTradingState copyState,
|
||||||
|
PolymarketApiService api,
|
||||||
PolymarketClobClient clob,
|
PolymarketClobClient clob,
|
||||||
ChannelWriter<CopySignal> signalWriter,
|
ChannelWriter<CopySignal> signalWriter,
|
||||||
ChannelWriter<ClosedTrade> closedTradeWriter,
|
ChannelWriter<ClosedTrade> closedTradeWriter,
|
||||||
@@ -50,6 +52,7 @@ namespace PolyTraderSharp.Services
|
|||||||
IMongoDatabase? db = null)
|
IMongoDatabase? db = null)
|
||||||
{
|
{
|
||||||
_state = state;
|
_state = state;
|
||||||
|
_copyState = copyState;
|
||||||
_api = api;
|
_api = api;
|
||||||
_clob = clob;
|
_clob = clob;
|
||||||
_signalWriter = signalWriter;
|
_signalWriter = signalWriter;
|
||||||
@@ -102,7 +105,7 @@ namespace PolyTraderSharp.Services
|
|||||||
_lastLivePoll = DateTime.UtcNow;
|
_lastLivePoll = DateTime.UtcNow;
|
||||||
await SyncMasterTraderPositionsAsync(stoppingToken);
|
await SyncMasterTraderPositionsAsync(stoppingToken);
|
||||||
_lastMasterPositionPoll = DateTime.UtcNow;
|
_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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -157,7 +160,7 @@ namespace PolyTraderSharp.Services
|
|||||||
return;
|
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;
|
if (activeTraders.Count == 0) return;
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
@@ -240,7 +243,7 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
public void TriggerFastBlockchainPoll(string txHash, string rpcUrl, string walletAddress)
|
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 (trader == null || !trader.IsActive) return;
|
||||||
|
|
||||||
if (_state.EnableBlockchainParser)
|
if (_state.EnableBlockchainParser)
|
||||||
@@ -391,7 +394,7 @@ namespace PolyTraderSharp.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void TriggerManualPoll(string walletAddress)
|
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 (trader != null && trader.IsActive)
|
||||||
{
|
{
|
||||||
if (!_activeWssPolls.TryAdd(trader.WalletAddress, true))
|
if (!_activeWssPolls.TryAdd(trader.WalletAddress, true))
|
||||||
@@ -625,12 +628,12 @@ namespace PolyTraderSharp.Services
|
|||||||
if (size > 0) exitPrice = (investment + realizedPnl) / size;
|
if (size > 0) exitPrice = (investment + realizedPnl) / size;
|
||||||
|
|
||||||
string orderKey = $"{acc.AccountId}_{asset}";
|
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";
|
string exitReason = soldByUs ? "Master Trader Sold" : "Manuell Geschlossen / System";
|
||||||
|
|
||||||
var ctRecord = new ClosedTrade
|
var ctRecord = new ClosedTrade
|
||||||
{
|
{
|
||||||
TradeId = _state.GetNextTradeId(),
|
TradeId = _copyState.GetNextTradeId(),
|
||||||
AccountId = acc.AccountId,
|
AccountId = acc.AccountId,
|
||||||
SourceTraderId = resolvedSourceId,
|
SourceTraderId = resolvedSourceId,
|
||||||
IsDemo = false,
|
IsDemo = false,
|
||||||
@@ -744,10 +747,10 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
// 1. Prüfe PendingOrderTimestamps (CopyTradingEngine hat diese Order kürzlich platziert)
|
// 1. Prüfe PendingOrderTimestamps (CopyTradingEngine hat diese Order kürzlich platziert)
|
||||||
string orderKey = $"{acc.AccountId}_{asset}";
|
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;
|
resolvedTraderId = pending.SourceTraderId;
|
||||||
if (_state.Traders.TryGetValue(resolvedTraderId, out var pendingTrader))
|
if (_copyState.Traders.TryGetValue(resolvedTraderId, out var pendingTrader))
|
||||||
{
|
{
|
||||||
resolvedTraderName = pendingTrader.DisplayName;
|
resolvedTraderName = pendingTrader.DisplayName;
|
||||||
resolvedTraderAddress = pendingTrader.WalletAddress;
|
resolvedTraderAddress = pendingTrader.WalletAddress;
|
||||||
@@ -920,12 +923,12 @@ namespace PolyTraderSharp.Services
|
|||||||
_logger.Info($"🏆 Live Market {removedPos.MarketQuestion} geschlossen! PnL: ${(realizedPnl):F2}");
|
_logger.Info($"🏆 Live Market {removedPos.MarketQuestion} geschlossen! PnL: ${(realizedPnl):F2}");
|
||||||
|
|
||||||
string orderKey = $"{acc.AccountId}_{removedPos.TokenId}";
|
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";
|
string exitReason = soldByUs ? "Master Trader Sold" : "Market Resolved";
|
||||||
|
|
||||||
var ctRecord = new ClosedTrade
|
var ctRecord = new ClosedTrade
|
||||||
{
|
{
|
||||||
TradeId = _state.GetNextTradeId(),
|
TradeId = _copyState.GetNextTradeId(),
|
||||||
AccountId = acc.AccountId,
|
AccountId = acc.AccountId,
|
||||||
SourceTraderId = removedPos.SourceTraderId,
|
SourceTraderId = removedPos.SourceTraderId,
|
||||||
IsDemo = false,
|
IsDemo = false,
|
||||||
@@ -943,7 +946,7 @@ namespace PolyTraderSharp.Services
|
|||||||
ExitReason = exitReason
|
ExitReason = exitReason
|
||||||
};
|
};
|
||||||
|
|
||||||
if (soldByUs) _state.PendingOrderTimestamps.TryRemove(orderKey, out _);
|
if (soldByUs) _copyState.PendingOrderTimestamps.TryRemove(orderKey, out _);
|
||||||
|
|
||||||
_processedClosures.TryAdd(duplicateKey, true);
|
_processedClosures.TryAdd(duplicateKey, true);
|
||||||
_closedTradeWriter.TryWrite(ctRecord);
|
_closedTradeWriter.TryWrite(ctRecord);
|
||||||
@@ -970,12 +973,12 @@ namespace PolyTraderSharp.Services
|
|||||||
_logger.Info($"🏆 Live Market {removedPos.MarketQuestion} aufgelöst (Fallback)! Auszahlung: ${(exitPrice * removedPos.Size):F2}");
|
_logger.Info($"🏆 Live Market {removedPos.MarketQuestion} aufgelöst (Fallback)! Auszahlung: ${(exitPrice * removedPos.Size):F2}");
|
||||||
|
|
||||||
string orderKey = $"{acc.AccountId}_{removedPos.TokenId}";
|
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";
|
string exitReason = soldByUs ? "Master Trader Sold" : "Market Resolved";
|
||||||
|
|
||||||
var ctRecord = new ClosedTrade
|
var ctRecord = new ClosedTrade
|
||||||
{
|
{
|
||||||
TradeId = _state.GetNextTradeId(),
|
TradeId = _copyState.GetNextTradeId(),
|
||||||
AccountId = acc.AccountId,
|
AccountId = acc.AccountId,
|
||||||
SourceTraderId = removedPos.SourceTraderId,
|
SourceTraderId = removedPos.SourceTraderId,
|
||||||
IsDemo = false,
|
IsDemo = false,
|
||||||
@@ -993,7 +996,7 @@ namespace PolyTraderSharp.Services
|
|||||||
ExitReason = exitReason
|
ExitReason = exitReason
|
||||||
};
|
};
|
||||||
|
|
||||||
if (soldByUs) _state.PendingOrderTimestamps.TryRemove(orderKey, out _);
|
if (soldByUs) _copyState.PendingOrderTimestamps.TryRemove(orderKey, out _);
|
||||||
|
|
||||||
_processedClosures.TryAdd(duplicateKey, true);
|
_processedClosures.TryAdd(duplicateKey, true);
|
||||||
_closedTradeWriter.TryWrite(ctRecord);
|
_closedTradeWriter.TryWrite(ctRecord);
|
||||||
@@ -1079,7 +1082,7 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
if (ct.IsCancellationRequested) break;
|
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;
|
continue;
|
||||||
|
|
||||||
var positionSizes = await _api.GetTraderPositionSizesAsync(trader.WalletAddress, tokenIds);
|
var positionSizes = await _api.GetTraderPositionSizesAsync(trader.WalletAddress, tokenIds);
|
||||||
@@ -1089,7 +1092,7 @@ namespace PolyTraderSharp.Services
|
|||||||
{
|
{
|
||||||
string key = $"{traderId}_{tokenId}";
|
string key = $"{traderId}_{tokenId}";
|
||||||
decimal shares = positionSizes.ContainsKey(tokenId) ? positionSizes[tokenId] : 0m;
|
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
|
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)
|
private async Task CleanupStaleOpenOrdersAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
var keysToProcess = _state.PendingOrderTimestamps.ToArray();
|
var keysToProcess = _copyState.PendingOrderTimestamps.ToArray();
|
||||||
if (keysToProcess.Length == 0) return;
|
if (keysToProcess.Length == 0) return;
|
||||||
|
|
||||||
foreach (var kvp in keysToProcess)
|
foreach (var kvp in keysToProcess)
|
||||||
@@ -1119,7 +1122,7 @@ namespace PolyTraderSharp.Services
|
|||||||
|
|
||||||
// Determine timeout based on trader category
|
// Determine timeout based on trader category
|
||||||
int timeoutMinutes = 30; // Default: 30 min
|
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
|
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)
|
// 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 _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using PolyTraderSharp.Models;
|
||||||
|
|
||||||
|
namespace PolyTraderSharp
|
||||||
|
{
|
||||||
|
public enum TradingMode
|
||||||
|
{
|
||||||
|
Inactive,
|
||||||
|
SellOnly,
|
||||||
|
Active
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
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<int, AccountState> 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<string, MarketData> MarketCache { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using PolyTraderSharp.Models;
|
||||||
|
|
||||||
|
namespace PolyTraderSharp
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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-<see cref="TradingState"/>.
|
||||||
|
/// </summary>
|
||||||
|
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<int, TrackedTrader> 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<TraderAnalyticsResult>)
|
||||||
|
public ConcurrentDictionary<int, List<TraderAnalyticsResult>> TraderAnalyticsCache { get; } = new();
|
||||||
|
|
||||||
|
// Tracks when live orders were placed for stale order cleanup.
|
||||||
|
// Key: "AccountId_TokenId", Value: (PlacedAt, SourceTraderId)
|
||||||
|
public ConcurrentDictionary<string, (DateTime PlacedAt, int SourceTraderId)> 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<string, (decimal Shares, DateTime LastUpdated)> MasterTraderPositions { get; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user