Files
PolyTraderSharp/tests/PolyTrader.Tests/SellLogicTests.cs
T
RichardandClaude Opus 4.8 395caad11a Phase 0.3: ProfitTarget-Take-Profit implementiert (dormant bei 9999)
- SellLogic.IsProfitTargetReached (pure, getestet): currentPrice >= entry*(1+pct/100);
  pct<=0 oder Default 9999 = inaktiv.
- Ladder-Start-Logik konsolidiert: SellLadderService.StartLadderAsync ist jetzt die
  gemeinsame Quelle fuer Master-SELLs (Engine) UND eigene Exits (Profit-Target).
  SellLadderService als Singleton+Hosted registriert; Engine + TraderMonitor
  injizieren es. Engine-SELL-Block ruft nur noch StartLadderAsync (verhaltensgleich).
- TraderMonitorService.CheckProfitTargetsAsync im 30s-Live-Sync: erreicht eine
  Live-Position ihre Schwelle, Exit ueber die Leiter (Startlimit = aktueller Preis,
  ExitReason "Profit Target"). PreRedeemLimit hat Vorrang. Dormant, da ProfitTarget
  projektweit 9999.

169 Tests gruen. Build/Smoke gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:54:21 +02:00

135 lines
4.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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));
}
[Theory]
[InlineData(true, 20)] // HF-Trader: schnelles Nachpreisen
[InlineData(false, 120)] // Standard
public void LadderIntervalSeconds_hf_is_faster(bool isHf, int expected)
{
Assert.Equal(expected, LadderIntervalSeconds(isHf));
}
[Theory]
[InlineData(0.50, 0.55, 10, true)] // genau an der Schwelle (entry*1.10)
[InlineData(0.50, 0.60, 10, true)] // darüber
[InlineData(0.50, 0.54, 10, false)] // darunter
[InlineData(0.50, 0.99, 9999, false)] // Default 9999 -> nie erreicht (inaktiv)
[InlineData(0.50, 0.99, 0, false)] // 0 -> deaktiviert
public void IsProfitTargetReached_threshold(double entry, double current, double pct, bool expected)
{
Assert.Equal(expected, IsProfitTargetReached((decimal)current, (decimal)entry, (decimal)pct));
}
[Fact]
public void IsProfitTargetReached_zero_entry_is_false()
{
Assert.False(IsProfitTargetReached(0.9m, 0m, 10m));
}
[Fact]
public void Ladder_walks_down_in_steps_until_floor()
{
// Simuliert den Service: von Startlimit in 3%-Schritten bis zum Floor, dann Stopp.
decimal reference = 0.99m;
decimal price = FirstLimit(reference, isHf: false, maxPriceDifferencePct: 2m); // 0.9702
decimal floor = Floor(reference, 15m); // 0.8415
int steps = 0;
while (!IsAtFloor(price, floor) && steps < 100)
{
price = NextPrice(price, LadderStepPct, floor);
steps++;
}
Assert.True(IsAtFloor(price, floor));
Assert.Equal(floor, price); // endet exakt auf dem Floor (geclamped)
Assert.InRange(steps, 1, 20); // terminiert in wenigen Schritten
}
}
}