Phase 5: AccountState-Split (Copytrading-Limits -> Modul)

- Copytrading-Detail-Einstellungen (PerMarketLimit, MaxBuyPrice, PerMasterLimit,
  Zeit-Limits, PreRedeemLimit, ProfitTarget, MaxPriceDifference) aus dem Core-
  AccountState in das Modul-Modell CopyTradingAccountSettings verschoben.
- CopyTradingState.AccountSettings + GetAccountSettings(accountId) (Default-safe).
- Consumer umgestellt: CopyTradingEngine, TraderMonitorService, PolymarketWssClient,
  frm_main lesen die Limits jetzt aus den Account-Settings.
- Neues Modul-Repo ICopyTradingAccountSettingsRepository (Collection ct_account_settings),
  in CopyTradingModule registriert.
- StartupHydration: Settings laden + EINMALIGE Migration der Alt-Limits aus dem
  Roh-accounts-Dokument (keine konfigurierten Limits gehen verloren).
- Build 0 Fehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-03 10:32:52 +02:00
co-authored by Claude Opus 4.8
parent 37f9d0fae2
commit 095c4b64aa
11 changed files with 208 additions and 60 deletions
@@ -210,6 +210,9 @@ namespace PolyTraderSharp.Services
private async Task ProcessAccountOrderAsync(AccountState account, TrackedTrader? trader, CopySignal signal, bool isNegRisk)
{
// Copytrading-Detail-Einstellungen (Limits) dieses Accounts.
var settings = _copyState.GetAccountSettings(account.AccountId);
var mode = account.IsDemo ? _state.DemoTradingMode : _state.LiveTradingMode;
if (mode == TradingMode.Inactive)
{
@@ -270,11 +273,11 @@ namespace PolyTraderSharp.Services
if (signal.Side == "BUY")
{
if (signal.Price > account.MaxBuyPrice)
if (signal.Price > settings.MaxBuyPrice)
{
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen (Risk Limit):\n" +
$" Konto: {account.Name}\n" +
$" Begründung: Preis (${signal.Price:F3}) übersteigt das MaxBuy Limit (${account.MaxBuyPrice:F3})");
$" Begründung: Preis (${signal.Price:F3}) übersteigt das MaxBuy Limit (${settings.MaxBuyPrice:F3})");
return;
}
@@ -282,7 +285,7 @@ namespace PolyTraderSharp.Services
decimal investedInMarket = activePositions.FirstOrDefault(p => p.TokenId == signal.TokenId)?.AmountUsd ?? 0m;
decimal minTrade = 1.0m;
decimal maxAllowed = account.TotalBalance * (account.PerMarketLimit / 100.0m);
decimal maxAllowed = account.TotalBalance * (settings.PerMarketLimit / 100.0m);
// Low Balance Bypass (Stufen-System) ALWAYS APPLIES
if (account.TotalBalance < 150m) maxAllowed = Math.Min(1.20m, Math.Max(account.AvailableBalance, 0m));
@@ -298,9 +301,9 @@ namespace PolyTraderSharp.Services
}
else
{
desiredLimitForSix = signal.Price * (1.0m + account.MaxPriceDifference / 100.0m);
desiredLimitForSix = signal.Price * (1.0m + settings.MaxPriceDifference / 100.0m);
}
decimal orderPriceForSix = Math.Min(desiredLimitForSix, account.MaxBuyPrice);
decimal orderPriceForSix = Math.Min(desiredLimitForSix, settings.MaxBuyPrice);
if (orderPriceForSix > 0.99m) orderPriceForSix = 0.99m;
decimal costSix = 6m * orderPriceForSix;
@@ -314,14 +317,14 @@ namespace PolyTraderSharp.Services
decimal investedInMaster = trader != null ? activePositions.Where(p => p.SourceTraderId == trader.Id).Sum(p => (decimal)p.AmountUsd) : 0m;
decimal maxAllowedPerMaster = account.TotalBalance * (account.PerMasterLimit / 100.0m);
decimal maxAllowedPerMaster = account.TotalBalance * (settings.PerMasterLimit / 100.0m);
if (trader != null && (investedInMaster + maxAmountToBuy) > maxAllowedPerMaster)
{
decimal pctInvested = account.TotalBalance > 0 ? (investedInMaster / account.TotalBalance) * 100m : 0m;
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen:\n" +
$" Konto: {account.Name}\n" +
$" Begründung: PerMasterLimit ({account.PerMasterLimit:F1}%) erreicht. Bisher investiert in '{trader.DisplayName}': ${investedInMaster:F2} ({pctInvested:F1}%).");
$" Begründung: PerMasterLimit ({settings.PerMasterLimit:F1}%) erreicht. Bisher investiert in '{trader.DisplayName}': ${investedInMaster:F2} ({pctInvested:F1}%).");
return;
}
@@ -335,25 +338,25 @@ namespace PolyTraderSharp.Services
if (hoursLeft < 6)
{
applicableTimeLimitPct = account.perMaxTime6h;
applicableTimeLimitPct = settings.perMaxTime6h;
timeframeLabel = "< 6h";
investedInTimeframe = openVals.Where(p => p.ExpiryDate.HasValue && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours < 6).Sum(p => (decimal)p.AmountUsd);
}
else if (hoursLeft < 24)
{
applicableTimeLimitPct = account.perMaxTime24h;
applicableTimeLimitPct = settings.perMaxTime24h;
timeframeLabel = "< 24h";
investedInTimeframe = openVals.Where(p => p.ExpiryDate.HasValue && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours >= 6 && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours < 24).Sum(p => (decimal)p.AmountUsd);
}
else if (hoursLeft < 72)
{
applicableTimeLimitPct = account.perMaxTime72h;
applicableTimeLimitPct = settings.perMaxTime72h;
timeframeLabel = "< 72h";
investedInTimeframe = openVals.Where(p => p.ExpiryDate.HasValue && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours >= 24 && (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours < 72).Sum(p => (decimal)p.AmountUsd);
}
else
{
applicableTimeLimitPct = account.perMaxTimeNone;
applicableTimeLimitPct = settings.perMaxTimeNone;
timeframeLabel = "> 72h";
investedInTimeframe = openVals.Where(p => !p.ExpiryDate.HasValue || (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours >= 72).Sum(p => (decimal)p.AmountUsd);
}
@@ -401,10 +404,10 @@ namespace PolyTraderSharp.Services
else
{
// Normaler Trader: prozentuales Limit aus Slave-Account Settings
desiredLimit = signal.Price * (1.0m + account.MaxPriceDifference / 100.0m);
desiredLimit = signal.Price * (1.0m + settings.MaxPriceDifference / 100.0m);
}
orderPrice = Math.Min(desiredLimit, account.MaxBuyPrice);
orderPrice = Math.Min(desiredLimit, settings.MaxBuyPrice);
if (orderPrice > 0.99m) orderPrice = 0.99m;
var exact = PolymarketClobClient.CalculateExactOrderAmounts(maxAmountToBuy, orderPrice, orderPrice, "BUY");
@@ -450,7 +450,7 @@ namespace PolyTraderSharp.Services
try
{
decimal expectedFillPrice = acc.PreRedeemLimit;
decimal expectedFillPrice = _copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit;
decimal amountUsdc = Math.Max(pos.Size * expectedFillPrice, 0.01m);
var result = await _clob.PlaceOrderAsync(acc, pos.TokenId, "SELL", amountUsdc, expectedFillPrice, "GTC", false, false);
@@ -703,7 +703,7 @@ namespace PolyTraderSharp.Services
try { _positionRepo.UpsertLive(acc.AccountId, existing); } catch { }
// Auto-Redeem Fallback via REST
if (acc.PreRedeemLimit > 0 && curPrice >= acc.PreRedeemLimit && acc.IsActive)
if (_copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit > 0 && curPrice >= _copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit && acc.IsActive)
{
string redeemKey = $"{acc.AccountId}_{asset}";
bool allowAttempt = true;
@@ -718,7 +718,7 @@ namespace PolyTraderSharp.Services
{
if (!acc.IsDemo && _state.LiveTradingMode == TradingMode.Active)
{
_logger.Trade($"🚨 [REST AUTO REDEEM] {acc.Name} | {existing.MarketQuestion} | Preis >= {acc.PreRedeemLimit}");
_logger.Trade($"🚨 [REST AUTO REDEEM] {acc.Name} | {existing.MarketQuestion} | Preis >= {_copyState.GetAccountSettings(acc.AccountId).PreRedeemLimit}");
_ = Task.Run(async () => await ExecuteRestAutoRedeemLive(acc, existing));
}
}