Testnetz: Micro-Order-Minimum + SELL-Proportionalität extrahiert; SELL-Leiter (Phase 0.1) vorbereitet
Weiter im Muster extrahieren->testen (verhaltensneutral, clob.md): - CopyTradingRisk.IsBelowPolymarketMinimum (Shares < 5.5 || USDC < 0.10) – BUY-Micro-Order-Filter verdrahtet + Tests. - SellLogic.SellProportion (signalSize/(remaining+signalSize), Denominator<=0 -> 0) + Konstante MinSignificantSellRatio (0.30); SELL-Proportionalitätscheck der Engine ruft sie jetzt (Charakterisierung, sichert Phase-2-Umbau ab). - SellLogic-Eskalationsleiter (Phase 0.1, NOCH NICHT verdrahtet): FirstLimit (HF-fest/prozentual), Floor (SellFloorPct), NextPrice (relative Stufe, auf Floor geclamped), IsAtFloor. Reine Grundlage fuer das spaetere SELL-Wiring. - Tests: CopyTradingRisk-Minimum (5 Faelle) + SellLogicTests (Proportion + Leiter). Gesamt 154 gruen. Build/Smoke gruen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6a8ecbc80a
commit
581755029a
@@ -18,6 +18,20 @@ namespace PolyTrader.Modules.CopyTrading.Logic
|
||||
/// <summary>Fester Limit-Aufschlag für HF-Trader (0,5 ¢).</summary>
|
||||
public const decimal HfLimitOffset = 0.005m;
|
||||
|
||||
/// <summary>Polymarket-Mindestgröße: wir filtern schon knapp über 5 Shares, um API-Fehler
|
||||
/// ("Size lower than the minimum: 5") zu vermeiden.</summary>
|
||||
public const decimal MinShares = 5.5m;
|
||||
|
||||
/// <summary>Absolute USDC-Untergrenze pro Order.</summary>
|
||||
public const decimal MinUsdc = 0.10m;
|
||||
|
||||
/// <summary>
|
||||
/// Liegt eine berechnete Order unter dem Polymarket-Minimum (Shares ODER USDC)?
|
||||
/// Solche Mikro-Orders werden vor dem API-Call verworfen.
|
||||
/// </summary>
|
||||
public static bool IsBelowPolymarketMinimum(decimal shares, decimal usdc) =>
|
||||
shares < MinShares || usdc < MinUsdc;
|
||||
|
||||
/// <summary>
|
||||
/// Limit-Preis einer BUY-Order: HF-Trader bekommen einen festen Aufschlag von 0.005,
|
||||
/// sonst einen prozentualen Aufschlag (<paramref name="maxPriceDifferencePct"/>) über dem
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
|
||||
namespace PolyTrader.Modules.CopyTrading.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// Reine SELL-Entscheidungs-/Preislogik. Teil davon (Proportionalität) ist 1:1 aus der
|
||||
/// aktuellen Engine übernommen (Charakterisierung, damit der Phase-2-Umbau abgesichert ist);
|
||||
/// die Eskalationsleiter ist die Grundlage für Phase 0.1 (noch NICHT in der Engine verdrahtet).
|
||||
/// </summary>
|
||||
public static class SellLogic
|
||||
{
|
||||
// ----- Proportionalität (aktuelles Verhalten, Schwelle hart 30 %) -----
|
||||
|
||||
/// <summary>Ab dieser Verkaufsquote des Masters kopieren wir den SELL (aktuell hart 30 %).</summary>
|
||||
public const decimal MinSignificantSellRatio = 0.30m;
|
||||
|
||||
/// <summary>
|
||||
/// Anteil, den dieser SELL am Master-Bestand VOR dem Verkauf ausmacht:
|
||||
/// <c>signalSize / (masterSharesRemaining + signalSize)</c>. Denominator ≤ 0 → 0.
|
||||
/// </summary>
|
||||
public static decimal SellProportion(decimal masterSharesRemaining, decimal signalSize)
|
||||
{
|
||||
decimal total = masterSharesRemaining + signalSize;
|
||||
if (total <= 0m) return 0m;
|
||||
return signalSize / total;
|
||||
}
|
||||
|
||||
// ----- Eskalationsleiter (Phase 0.1, noch nicht verdrahtet) -----
|
||||
|
||||
/// <summary>
|
||||
/// Erster SELL-Limit-Preis (GTD): HF-Trader fester Abschlag von 0.005 unter dem Master-Exit,
|
||||
/// sonst prozentualer Abschlag (<paramref name="maxPriceDifferencePct"/>).
|
||||
/// </summary>
|
||||
public static decimal FirstLimit(decimal referencePrice, bool isHf, decimal maxPriceDifferencePct) =>
|
||||
isHf ? referencePrice - CopyTradingRisk.HfLimitOffset
|
||||
: referencePrice * (1.0m - maxPriceDifferencePct / 100.0m);
|
||||
|
||||
/// <summary>
|
||||
/// Absolute Untergrenze der Leiter: <c>referencePrice × (1 − SellFloorPct/100)</c>.
|
||||
/// Darunter wird nicht mehr verkauft (Position halten statt Exit-Liquidity zu werden).
|
||||
/// </summary>
|
||||
public static decimal Floor(decimal referencePrice, decimal sellFloorPct) =>
|
||||
referencePrice * (1.0m - sellFloorPct / 100.0m);
|
||||
|
||||
/// <summary>
|
||||
/// Nächste Stufe nach ausbleibendem Fill: aktuellen Preis um <paramref name="stepPct"/> %
|
||||
/// relativ senken, aber nie unter den <paramref name="floor"/>.
|
||||
/// </summary>
|
||||
public static decimal NextPrice(decimal currentPrice, decimal stepPct, decimal floor) =>
|
||||
Math.Max(currentPrice * (1.0m - stepPct / 100.0m), floor);
|
||||
|
||||
/// <summary>Leiter erschöpft: aktueller Preis hat den Floor erreicht/unterschritten.</summary>
|
||||
public static bool IsAtFloor(decimal currentPrice, decimal floor) => currentPrice <= floor;
|
||||
}
|
||||
}
|
||||
@@ -383,7 +383,7 @@ namespace PolyTraderSharp.Services
|
||||
// ===== MICRO-ORDER FILTER: Polymarket Minimum Size Enforcement =====
|
||||
// Polymarket lehnt Orders mit < 5 Shares ab ("Size lower than the minimum: 5").
|
||||
// Statt die API zu belasten und Fehler-Logs zu erzeugen, filtern wir hier sofort.
|
||||
if (exact.shares < 5.5m || exact.usdc < 0.10m)
|
||||
if (CopyTradingRisk.IsBelowPolymarketMinimum(exact.shares, exact.usdc))
|
||||
{
|
||||
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] gestoppt:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
@@ -431,8 +431,9 @@ namespace PolyTraderSharp.Services
|
||||
if (masterShares > 0 && signal.Size > 0)
|
||||
{
|
||||
// Calculate what percentage of the master's known position this SELL represents
|
||||
decimal sellRatio = signal.Size / (masterShares + signal.Size); // +signal.Size because the position was already reduced
|
||||
if (sellRatio < 0.30m)
|
||||
// (Proportionalität: SellLogic, unit-getestet)
|
||||
decimal sellRatio = SellLogic.SellProportion(masterShares, signal.Size);
|
||||
if (sellRatio < SellLogic.MinSignificantSellRatio)
|
||||
{
|
||||
_logger.TradeReasoning($"📊 Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
|
||||
@@ -190,5 +190,18 @@ namespace PolyTrader.Tests
|
||||
// available großzügig, damit die Stufen-Caps nicht durch Verfügbarkeit greifen
|
||||
Assert.Equal((decimal)expected, MaxPerMarket((decimal)balance, 100000m, 5m));
|
||||
}
|
||||
|
||||
// ---------------- IsBelowPolymarketMinimum ----------------
|
||||
|
||||
[Theory]
|
||||
[InlineData(5.5, 1.0, false)] // genau an der Share-Grenze -> ok
|
||||
[InlineData(5.4, 1.0, true)] // zu wenige Shares
|
||||
[InlineData(6.0, 0.10, false)] // genau an der USDC-Grenze -> ok
|
||||
[InlineData(6.0, 0.09, true)] // zu wenig USDC
|
||||
[InlineData(100.0, 50.0, false)]
|
||||
public void IsBelowPolymarketMinimum_enforces_share_and_usdc_floor(double shares, double usdc, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, IsBelowPolymarketMinimum((decimal)shares, (decimal)usdc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
using PolyTrader.Modules.CopyTrading.Logic;
|
||||
using Xunit;
|
||||
using static PolyTrader.Modules.CopyTrading.Logic.SellLogic;
|
||||
|
||||
namespace PolyTrader.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Sicherheitsnetz für die SELL-Logik: Proportionalität (aktuelles Verhalten, Phase-2-Umbau)
|
||||
/// und die Eskalationsleiter (Phase 0.1 – Preis-Stufen, Floor-Clamping).
|
||||
/// </summary>
|
||||
public class SellLogicTests
|
||||
{
|
||||
// ---------------- SellProportion ----------------
|
||||
|
||||
[Fact]
|
||||
public void SellProportion_is_share_over_total_before_sell()
|
||||
{
|
||||
// Master hält noch 300, verkauft 200 -> 200/500 = 0.40
|
||||
Assert.Equal(0.40m, SellProportion(masterSharesRemaining: 300m, signalSize: 200m));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SellProportion_small_partial_is_below_threshold()
|
||||
{
|
||||
// 498 Rest, 2 verkauft -> 0.004 < 0.30
|
||||
var ratio = SellProportion(498m, 2m);
|
||||
Assert.True(ratio < MinSignificantSellRatio);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SellProportion_full_exit_is_one()
|
||||
{
|
||||
Assert.Equal(1.0m, SellProportion(0m, 10m));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0)] // gar nichts
|
||||
[InlineData(-5, 5)] // Denominator 0
|
||||
[InlineData(-10, 5)] // Denominator negativ
|
||||
public void SellProportion_guards_non_positive_denominator(double remaining, double signal)
|
||||
{
|
||||
Assert.Equal(0m, SellProportion((decimal)remaining, (decimal)signal));
|
||||
}
|
||||
|
||||
// ---------------- Eskalationsleiter ----------------
|
||||
|
||||
[Fact]
|
||||
public void FirstLimit_normal_trader_applies_percentage_discount()
|
||||
{
|
||||
// 0.99 * (1 - 2%) = 0.9702
|
||||
Assert.Equal(0.9702m, FirstLimit(0.99m, isHf: false, maxPriceDifferencePct: 2m));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstLimit_hf_trader_applies_fixed_half_cent_discount()
|
||||
{
|
||||
Assert.Equal(0.985m, FirstLimit(0.99m, isHf: true, maxPriceDifferencePct: 99m));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Floor_is_reference_minus_floor_pct()
|
||||
{
|
||||
// 0.99 * (1 - 15%) = 0.8415
|
||||
Assert.Equal(0.8415m, Floor(0.99m, 15m));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextPrice_steps_down_relative_when_above_floor()
|
||||
{
|
||||
// 0.90 * (1 - 3%) = 0.873, über Floor 0.80
|
||||
Assert.Equal(0.873m, NextPrice(0.90m, stepPct: 3m, floor: 0.80m));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextPrice_clamps_to_floor()
|
||||
{
|
||||
// 0.82 * 0.97 = 0.7954 -> unter Floor -> 0.80
|
||||
Assert.Equal(0.80m, NextPrice(0.82m, 3m, 0.80m));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsAtFloor_is_inclusive()
|
||||
{
|
||||
Assert.True(IsAtFloor(0.80m, 0.80m));
|
||||
Assert.True(IsAtFloor(0.79m, 0.80m));
|
||||
Assert.False(IsAtFloor(0.81m, 0.80m));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user