Files
IBKRTrader/tests/IBKRTrader.Tests/Trading/RiskServiceTests.cs
T
Richard c20538c8d8 @
R1: Umbau auf Multi-Projekt-Struktur (Vorbild PolytraderSharp)

Kurskorrektur: Vorbild ist PolytraderSharp (C#), nicht die veraltete Python-Version.
Reiner Strukturumbau, Verhalten unveraendert.

- src/IBKRTrader.Core (classlib): Core-Code + UI-Contract (ModuleFormBase, WindowManager)
- src/IBKRTrader.Modules.CongressTrading (classlib, referenziert nur Core)
- Root: IBKRTrader.App (WinExe) referenziert Core + Modul
- tests/IBKRTrader.Tests: Referenzen auf Core + Modul
- Neue .slnx; ungenutztes HtmlAgilityPack entfernt
- docs/ARCHITECTURE.md: korrigierter Ziel-Stand + R-Phasenplan
- Build + 38/38 Tests gruen; App startet

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-27 20:26:25 +02:00

143 lines
4.3 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 FluentAssertions;
using IBKRTrader.Core.Trading;
namespace IBKRTrader.Tests.Trading;
[Trait("cat", "unit")]
public class RiskServiceTests
{
private readonly RiskService _risk = new();
// MaxTrade 5 %, MaxModul 20 %, MaxSlippage 5 %
private static readonly RiskParameters DefaultParams = new(5m, 20m, 5m);
private static TradeSignal Buy(decimal? limit = null, decimal? notional = null) => new()
{
Symbol = "AAPL", Side = TradeSide.Buy, SourceModule = "CT",
LimitPrice = limit, SuggestedNotional = notional
};
private static TradeSignal Sell() => new()
{
Symbol = "AAPL", Side = TradeSide.Sell, SourceModule = "CT"
};
[Fact]
public void Buy_SizesByMaxTradePercent()
{
// NetLiq 10.000 × 5 % = 500 max Nominal; Kurs 100 → 5 Stück
var ctx = new RiskContext { Price = 100m, NetLiquidation = 10_000m };
var d = _risk.Evaluate(Buy(), ctx, DefaultParams);
d.Approved.Should().BeTrue();
d.Quantity.Should().Be(5);
}
[Fact]
public void Buy_UsesSuggestedNotional_WhenSmallerThanMax()
{
// Wunsch 200 < Max 500; Kurs 100 → 2 Stück
var ctx = new RiskContext { Price = 100m, NetLiquidation = 10_000m };
var d = _risk.Evaluate(Buy(notional: 200m), ctx, DefaultParams);
d.Quantity.Should().Be(2);
}
[Fact]
public void Buy_InvalidPrice_Rejected()
{
var ctx = new RiskContext { Price = 0m, NetLiquidation = 10_000m };
_risk.Evaluate(Buy(), ctx, DefaultParams).Approved.Should().BeFalse();
}
[Fact]
public void Buy_ZeroAccount_Rejected()
{
var ctx = new RiskContext { Price = 100m, NetLiquidation = 0m };
_risk.Evaluate(Buy(), ctx, DefaultParams).Approved.Should().BeFalse();
}
[Fact]
public void Buy_QuantityBelowOne_Rejected()
{
// NetLiq 100 × 5 % = 5 max Nominal; Kurs 100 → 0 Stück
var ctx = new RiskContext { Price = 100m, NetLiquidation = 100m };
var d = _risk.Evaluate(Buy(), ctx, DefaultParams);
d.Approved.Should().BeFalse();
d.Reason.Should().Contain("< 1");
}
[Fact]
public void Buy_ExceedsModuleLimit_Rejected()
{
// MaxTrade 50 % → 5.000 Nominal, Kurs 100 → 50 Stück = 5.000
// Modul-Limit 20 % × 10.000 = 2.000 → abgelehnt
var ctx = new RiskContext { Price = 100m, NetLiquidation = 10_000m };
var pars = new RiskParameters(50m, 20m, 5m);
var d = _risk.Evaluate(Buy(), ctx, pars);
d.Approved.Should().BeFalse();
d.Reason.Should().Contain("Modul-Limit");
}
[Fact]
public void Buy_ExistingExposureCountsTowardModuleLimit()
{
// Kurs 100, Max 5 % → 5 Stück (500). Bereits 1.700 Exposure.
// Projektiert 2.200 > Limit 2.000 → abgelehnt.
var ctx = new RiskContext { Price = 100m, NetLiquidation = 10_000m, ModuleExposure = 1_700m };
_risk.Evaluate(Buy(), ctx, DefaultParams).Approved.Should().BeFalse();
}
[Fact]
public void Buy_LimitOrder_SlippageTooHigh_Rejected()
{
// Limit 100, Kurs 110 → 10 % > 5 %
var ctx = new RiskContext { Price = 110m, NetLiquidation = 10_000m };
var d = _risk.Evaluate(Buy(limit: 100m), ctx, DefaultParams);
d.Approved.Should().BeFalse();
d.Reason.Should().Contain("Slippage");
}
[Fact]
public void Buy_LimitOrder_SlippageWithinTolerance_Approved()
{
// Limit 100, Kurs 104 → 4 % < 5 %
var ctx = new RiskContext { Price = 104m, NetLiquidation = 10_000m };
_risk.Evaluate(Buy(limit: 100m), ctx, DefaultParams).Approved.Should().BeTrue();
}
[Fact]
public void Sell_WithPosition_ClosesQuantity()
{
var ctx = new RiskContext { Price = 100m, NetLiquidation = 10_000m, ExistingQuantity = 12 };
var d = _risk.Evaluate(Sell(), ctx, DefaultParams);
d.Approved.Should().BeTrue();
d.Quantity.Should().Be(12);
}
[Fact]
public void Sell_WithoutPosition_Rejected()
{
var ctx = new RiskContext { Price = 100m, NetLiquidation = 10_000m, ExistingQuantity = 0 };
var d = _risk.Evaluate(Sell(), ctx, DefaultParams);
d.Approved.Should().BeFalse();
d.Reason.Should().Contain("Keine Position");
}
}