CLOB-Sicherheitsnetz: BUY-Risikologik extrahiert + breit getestet; PreRedeemLimit-Skala korrigiert

Ziel (Richard): vor den CLOB-Eingriffen ein umfangreiches Testnetz, damit künftige
Änderungen keine neuen Fehler einschleusen. Reiner, verhaltensneutraler Umbau –
alte Version liegt als Rollback in Git (Commit 38f609e), siehe .agents/rules/clob.md.

- Neue pure Klasse CopyTradingRisk (Logic/): CalculateBuyOrderPrice (HF-fest /
  prozentual, gedeckelt durch MaxBuyPrice + harte 0.99), ResolveTimeBucket/
  TimeLimitPct/TimeBucketLabel/IsPositionInBucket (Zeitfenster-Exposure),
  MaxPerMarket (Markt-Budget inkl. Low-Balance-Bypass-Stufen).
- CopyTradingEngine BUY-Pfad ruft diese Funktionen jetzt statt Inline-Mathematik
  (1:1-Semantik, dedupliziert die doppelte Order-Preis-Berechnung).
- CopyTradingRiskTests: 38 Fälle über alle Zweige/Grenzwerte (HF vs. normal,
  MaxBuy-/0.99-Deckel, Bucket-Grenzen 6/24/72h, null/expired Expiry,
  Balance-Stufen 150/500). Gesamt 124 Tests grün.

Fix: PreRedeemLimit-Skalen-Korrektur (Migration FixPreRedeemLimitScale): Alt-Werte
> 1 (z. B. 99.5) werden /100 (0.995); 0 bleibt deaktiviert. Auf MySQL angewendet.

Build/Smoke grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-07 10:03:53 +02:00
co-authored by Claude Opus 4.8
parent 38f609ed31
commit 360f264ed8
5 changed files with 605 additions and 63 deletions
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using PolyTraderSharp.Models;
using PolyTrader.Modules.CopyTrading.Logic;
using System.Collections.Concurrent;
using System.Linq;
@@ -292,26 +293,14 @@ namespace PolyTraderSharp.Services
decimal investedInMarket = activePositions.FirstOrDefault(p => p.TokenId == signal.TokenId)?.AmountUsd ?? 0m;
decimal minTrade = 1.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));
else if (account.TotalBalance < 500m) maxAllowed = Math.Min(3.0m, Math.Max(account.AvailableBalance, 0m));
// Markt-Budget inkl. Low-Balance-Bypass (CopyTradingRisk, unit-getestet)
decimal maxAllowed = CopyTradingRisk.MaxPerMarket(account.TotalBalance, account.AvailableBalance, settings.PerMarketLimit);
if (_copyState.SixSharesMinimum && account.TotalBalance < 500m)
{
// Adjust maxAllowed to cover at least 6 shares * order limit price.
decimal desiredLimitForSix;
if (trader != null && trader.Category == "HF")
{
desiredLimitForSix = signal.Price + 0.005m;
}
else
{
desiredLimitForSix = signal.Price * (1.0m + settings.MaxPriceDifference / 100.0m);
}
decimal orderPriceForSix = Math.Min(desiredLimitForSix, settings.MaxBuyPrice);
if (orderPriceForSix > 0.99m) orderPriceForSix = 0.99m;
decimal orderPriceForSix = CopyTradingRisk.CalculateBuyOrderPrice(
signal.Price, trader != null && trader.Category == "HF", settings.MaxPriceDifference, settings.MaxBuyPrice);
decimal costSix = 6m * orderPriceForSix;
if (costSix > maxAllowed)
@@ -335,38 +324,15 @@ namespace PolyTraderSharp.Services
return;
}
// Time Limit Restriktion
double hoursLeft = signal.EndDate.HasValue ? (signal.EndDate.Value - DateTime.UtcNow).TotalHours : 999999;
decimal applicableTimeLimitPct;
decimal investedInTimeframe = 0m;
string timeframeLabel = "";
var openVals = activePositions;
if (hoursLeft < 6)
{
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 = 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 = 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 = settings.perMaxTimeNone;
timeframeLabel = "> 72h";
investedInTimeframe = openVals.Where(p => !p.ExpiryDate.HasValue || (p.ExpiryDate.Value - DateTime.UtcNow).TotalHours >= 72).Sum(p => (decimal)p.AmountUsd);
}
// Time Limit Restriktion (Zeitfenster-Logik: CopyTradingRisk, unit-getestet)
var nowUtc = DateTime.UtcNow;
double hoursLeft = signal.EndDate.HasValue ? (signal.EndDate.Value - nowUtc).TotalHours : 999999;
var timeBucket = CopyTradingRisk.ResolveTimeBucket(hoursLeft);
decimal applicableTimeLimitPct = CopyTradingRisk.TimeLimitPct(timeBucket, settings);
string timeframeLabel = CopyTradingRisk.TimeBucketLabel(timeBucket);
decimal investedInTimeframe = activePositions
.Where(p => CopyTradingRisk.IsPositionInBucket(p.ExpiryDate, timeBucket, nowUtc))
.Sum(p => (decimal)p.AmountUsd);
decimal maxAllowedTimeframe = account.TotalBalance * (applicableTimeLimitPct / 100.0m);
@@ -402,20 +368,9 @@ namespace PolyTraderSharp.Services
return;
}
decimal desiredLimit;
if (trader != null && trader.Category == "HF")
{
// HF Trader: festes 0.5 Cent (0.005) Limit
desiredLimit = signal.Price + 0.005m;
}
else
{
// Normaler Trader: prozentuales Limit aus Slave-Account Settings
desiredLimit = signal.Price * (1.0m + settings.MaxPriceDifference / 100.0m);
}
orderPrice = Math.Min(desiredLimit, settings.MaxBuyPrice);
if (orderPrice > 0.99m) orderPrice = 0.99m;
// Limit-Preis (HF-fest / prozentual, gedeckelt) CopyTradingRisk, unit-getestet
orderPrice = CopyTradingRisk.CalculateBuyOrderPrice(
signal.Price, trader != null && trader.Category == "HF", settings.MaxPriceDifference, settings.MaxBuyPrice);
var exact = PolymarketClobClient.CalculateExactOrderAmounts(maxAmountToBuy, orderPrice, orderPrice, "BUY");
if (exact.shares <= 0 || exact.usdc > account.AvailableBalance)